1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FabfibというボードゲームのシミュレータをPythonで書いているという話

Last updated at Posted at 2018-12-02

Fabfibシミュレータfor研究

Fabfibというゲームの研究用の(プレイ用でない)シミュレータを2017年くらいから書いています。プレイをしたい場合はFabfibの購入をよろしくお願いします。

Fabfibとは

自身のポートフォリオサイトに詳細を書いています。

動機

宣言(call)捨て札(discard)およびダウト(doubt)のそれぞれの戦略について興味を持ちました。

どこまで書いたか

自分がプレイする時にどのような戦略を採用しているかを参考に、それぞれに基本的な戦略を定義しました。それを元にゲームを回すところまでを書きました。

何を使ったか

依存している外部ライブラリはnumpyのみで、他はPythonの標準ライブラリとなっています。Pythonは3系です。

pip3 install numpy

なぜnumpyを使ったかと言えば、numpyの行列で戦況を格納すれば、分析するときに便利だと考えたからです。

Call Strategy

前の宣言よりも低い宣言は無効です。いつも前の宣言よりも高い手札を引けるとは限らないので、Call Strategyでは、状況に応じて嘘の宣言をすることが求められます。

「捨てた札を引き直した結果、より高い数字の札を引いた。」と見せかける戦略を基本的な戦略として採用しています。

問題は、どのくらい高い数字に見せかけるかです。これを数理的に考えると、ポアソン分布に基づく乱数のような構造をしていると考え、採用しました。

例えば、4を捨てて6を引いたと見せかけるより、9を引いたと見せかける方が心理的な障壁が高いと考えられます。確率を考えるとより低い数字を引く確率が高いとは言えませんが、次の人になんとか受け取ってもらおうという心理を考えると、一定の合理性があるように感じます。

採用したロジックはこんな感じです。

class BasicLogic(CallStrategy):
    def __init__(self, Number, DiscardCount, Hand):
        super().__init__(Number, DiscardCount, Hand)

    def call_check(self):
        """Basic call_check method
        >>> import numpy as np
        >>> RegularCase = BasicLogic(431,1,np.array([6,3,1]))
        >>> print(RegularCase.call_check() == 631)
        True
        >>> IregularCase = BasicLogic(831,1,np.array([6,3,1]))
        >>> print(631 < IregularCase.call_check() <= 999)
        True
        >>> NineCase = BasicLogic(995,1,np.array([9,9,4]))
        >>> print(995 < NineCase.call_check() <= 999)
        True
        >>> AllChange = BasicLogic(654,3,np.array([4,3,1]))
        >>> print(654 < AllChange.call_check() <= 999)
        True
        """
        EvaluatedValue = super().evaluate_hand(self.Hand)

        # if real hand's value is weak,
        # Calling fake number based on Game's State
        if self.Number >= EvaluatedValue:

            # Leaves to change is based on NonNines Count of
            # Game's Number or DiscardCount.
            ToCall = np.array([int(i) for i in str(self.Number)])
            ToCall = np.sort(ToCall)  # ascending order
            NonNines = len(np.where(ToCall != 9)[0])
            LeavesToChange = min(self.DiscardCount, NonNines)

            for i in range(0, LeavesToChange - 1):
                ToCall[i] = np.random.poisson(lam=4)
                if ToCall[i] > 9:
                    ToCall[i] = 9

            ToCall[LeavesToChange - 1] += 1
            ToCall[LeavesToChange - 1] += np.random.poisson(lam=0.2)
            if ToCall[LeavesToChange - 1] > 9:
                ToCall[LeavesToChange - 1] = 9

            return super().evaluate_hand(np.sort(ToCall)[::-1])

        else:
            return EvaluatedValue

Discard Strategy

受け取った手札からどれを捨てるのかを決定する戦略です。

基本的な戦略として採用しているのはシンプルな戦略で、まず6より小さい数字は全て捨て、もし一番大きい数字と一番小さい数字の差が4未満であれば、9以外を全部捨てます。

例えば876という手札であれば、全て捨てて9を1枚以上引けば良いので、その確率にかけます。まずまずの順当な戦略と言えると思います。

採用したロジックはこんな感じです。

class BasicLogic(DiscardStrategy):
    """Players on BasicLogic strategy discard ...

    less then 6, because a high possibility to draw any of 5-9.
    If the difference of Max and Min is less then 4, other then nine.
    To raise the posibility of 9.
    """

    def __init__(self, Hand):
        super().__init__(Hand)
        self.Max = np.max(Hand)
        self.Min = np.min(Hand)

    def discard_check(self):
        """Basic discard check logic

        >>> import numpy as np
        >>> CloseCase = BasicLogic(np.array([6,5,4]))
        >>> CloseCase.discard_check()
        array([5, 4, 6])
        """
        self.less_then(6)
        if((self.Max - self.Min) < 4):
            self.less_then(9)
        return self.Discard

    def less_then(self, Number):
        self.Discard = np.append(self.Discard, self.Hand[self.Hand < Number])
        self.Hand = self.Hand[self.Hand >= Number]

Doubt Strategy

宣言に対してダウトを仕掛ける戦略です。ダウトを仕掛ける場合、ダメージを受けるリスクを負うので、ダウトを仕掛ける際は慎重になります。嘘をついてる可能性が高いと判断しても、宣言されている数字が低ければ受けとることがあり得るでしょう。

つまりダウトを仕掛ける判断には、宣言されている数字が重要になります。以下の式を用いてダウト指数とし、状況を加味してそれを補正する方法を採用しました。

採用したロジックはこんな感じです。

class BasicLogic(DoubtStrategy):
    def __init__(self, Call, DiscardCount):
        super().__init__(Call, DiscardCount)

    def doubt_check(self):
        """Basic doubt_check
        >>> import numpy as np
        >>> TwoDiscardCase = BasicLogic(992,2)
        >>> type(TwoDiscardCase.doubt_check())
        <class 'bool'>
        >>> ThreeDiscardCase = BasicLogic(982,3)
        >>> type(ThreeDiscardCase.doubt_check())
        <class 'bool'>
        """
        DoubtIndex = 0.0
        DoubtIndex += (np.random.rand() * 0.2 + 0.8) * \
            (((self.Call - 700) / 300) ** 3)

        if self.DiscardCount == 2:
            DoubtIndex += 0.1
            if self.Call >= 990:
                DoubtIndex += 0.6

        if self.DiscardCount == 3:
            DoubtIndex += 0.3
            if self.Call >= 900:
                DoubtIndex += 0.3

        if DoubtIndex > 0.85:
            self.Doubt = True

        return self.Doubt

動かしかた

numpyがインストールされていることを前提とします。cloneします。

git clone https://github.com/say-ya-sigma/fabfib.git
cd fabfib

game.pyで動かします。引数はゲーム数です。

python3 game.py 10
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?