2
0

More than 3 years have passed since last update.

「逆転オセロニア」を画像解析して自動ダメージ計算 part4 (ダメージ計算編)

Last updated at Posted at 2019-11-29

逆転オセロニア画面を解析して自動でダメージ計算

逆転オセロニアとはオセロのルールで駒を置いていき、スキルやコンボを駆使して相手のHPを削ったら勝ちという対戦型のスマホゲームアプリになります。

そのためダメージ計算はかなり重要な部分になってきます。

詳しくは

逆転オセロニア最速攻略wiki

で確認してみてください。

その逆転オセロニアのダメージ計算を自動で行うためのプログラムを1年ほど前に作成していましたが、その時の作業手順を思い出しながら少しだけ解説していこうと思います。



前回までの記事

「逆転オセロニア」を画像解析して自動ダメージ計算 part1 (iPhone PC表示編)

「逆転オセロニア」を画像解析して自動ダメージ計算 part2 (文字識別編)

「逆転オセロニア」を画像解析して自動ダメージ計算 part3 (キャラ識別編)

前回までの記事では「逆転オセロニア」をPCで表示し、PythonでATKの数値を読み込みと手駒のキャラ識別までの手順を紹介しました。

今回の記事

今回の記事は、逆転オセロニアのメイン処理と思われるオセロとダメージ計算部分についてのプログラムについて解説したいと思います。

自動で画像解析をする部分をメインで紹介したいと思っていたので、オセロ部分とダメージ計算(一部分)のロジックについては、簡単な説明にしたいと思います。

通常のオセロ部分のロジック+α

逆転オセロニアの基礎部分はオセロなので、オセロ部分のロジックも記載します。

オセロのプログラムの詳細については「python オセロ」や「オセロ プログラム」などでググってみてください。
逆転オセロニアのダメージ計算の考え方を知りたい方は「オセロニア ダメージ計算」などでググると詳しく解説しているサイトがいくつか出てきますので、そちらでも確認してみてください。

オセロニアのダメージ計算用クラス

from decimal import Decimal

# スキル(コンボ)定義用クラス
class Skill:
    def __init__(self, _type, **mapping):
        self.type = _type
        if "one" in mapping: self.one = Decimal(mapping['one'])
        if "max" in mapping: self.max = Decimal(mapping['max'])
        if "premise" in mapping: self.premise = mapping['premise']

BLANK = 0
BLACK = 1
WHITE = -1
BOARD_SIZE = 6
XS = ['A', 'B', 'C', 'D', 'E', 'F']
YS = ['1', '2', '3', '4', '5', '6']
OFFSET = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
DECK = {'0751': {'name': 'デネブ', 'skill': Skill('aura'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1926': {'name': '竜フェリヤ', 'skill': Skill('fixed', max='2'), 'combo': Skill('fixed', max='2.2')},
        '1719': {'name': 'ジェンイー', 'skill': Skill('my_board', premise=12, max='1.9'), 'combo': Skill('whisper', one='1.3', max='2.2')},
        '3414': {'name': 'ルーシュ', 'skill': Skill('whisper', one='1.3', max='2'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '3133': {'name': 'ランメリー', 'skill': Skill('fixed', max='1.9'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '3001': {'name': '京極', 'skill':Skill('fixed', max='1.8'), 'combo': Skill('whisper', one='1.3', max='1.6')},
        '2968': {'name': 'イモ', 'skill': Skill('aura'), 'combo': Skill('mai', premise=2, max='1.6')},
        '2250': {'name': '雛クロリス', 'skill': Skill('fixed', max='2.1'), 'combo': Skill('mai', premise=2, max='1.6')},
        '2206': {'name': 'ラウラ', 'skill': Skill('whisper', one='1.3', max='1.9'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '1933': {'name': '竜守護者', 'skill': Skill('fixed', max='1.7'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1892': {'name': 'エルツドラッヘ', 'skill': Skill('my_board', premise=8, max='1.7'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1646': {'name': '呂蒙', 'skill': Skill('previous', premise=1, max='1.3'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '1436': {'name': 'グレリオ', 'skill': Skill('fixed', max='1.5'), 'combo': Skill('fixed', max='1.6')},
        '0848': {'name': 'ファイドレ', 'skill': Skill('mai', premise=2, max='1.8'), 'combo': Skill('whisper', one='1.3', max='2')},
        '0847': {'name': 'ランタイ', 'skill': Skill('aura'), 'combo': Skill('fixed', max='1.4')},
        '0755': {'name': 'クロリス', 'skill': Skill('fixed', max='1.8'), 'combo': Skill('fixed', max='1.6')}}

# オセロニア ダメージ計算用クラス
class Damellonia:
    def __init__(self, my_stone=BLACK):
        self.cells = [[0, 0, 0, 0, 0, 0],
                     [0, 0, 0, 0, 0, 0],
                     [0, 0,-1, 1, 0, 0],
                     [0, 0, 1,-1, 0, 0],
                     [0, 0, 0, 0, 0, 0],
                     [0, 0, 0, 0, 0, 0]] # 現在の盤面の状況
        self.current = BLACK # 現在の順番
        self.player = my_stone # 自分の石の色
        self.boardPiece = [] # 盤面の自分の駒
        self.puts = []   # 現在置ける位置
        self.putInfo = []   # 置いた場合の情報
        self.previousCnt = 0   # 直前に返された枚数
        self.atks = [None,None,None,None] # 画面から識別したATKの値
        self.tegomas = [None,None,None,None] # 画面から識別した手駒番号
        for i in range(BOARD_SIZE):
            self.boardPiece.append([None for i in range(BOARD_SIZE)])
            self.putInfo.append([None for i in range(BOARD_SIZE)])
        self.allCheckStone()
        self.boardPrint()

    # 置ける石をチェック
    def checkStone(self, x, y):
        if self.cells[x][y] != BLANK:
            return
        flippable = []
        cmbs = []
        for dx, dy in OFFSET:
            tmp = []
            depth = 0
            while(True):
                depth += 1
                rx = x + (dx * depth)
                ry = y + (dy * depth)
                if 0 <= rx < BOARD_SIZE and 0 <= ry < BOARD_SIZE:
                    request = self.cells[rx][ry]
                    if request == BLANK:
                        break
                    if request == self.current:
                        if tmp != []:
                            flippable.extend(tmp)
                            if self.boardPiece[rx][ry] != None:
                                cmbs.append(self.boardPiece[rx][ry])
                        break
                    else:
                        tmp.append((rx, ry))
                else:
                    break  
        if flippable != []:
            # 駒の置ける位置や置いた場合の情報を退避
            self.puts.append((x, y))
            self.putInfo[x][y]={'flippable': flippable, 'cnt': len(flippable) , 'combos': cmbs}

    # 盤面全ての置ける石をチェック
    def allCheckStone(self):
        self.puts = []
        self.putInfo = []
        for i in range(BOARD_SIZE):
            self.putInfo.append([None for i in range(BOARD_SIZE)])
        for x in range(BOARD_SIZE):
            for y in range(BOARD_SIZE):
                self.checkStone(x, y)

    # 石を置く
    def putStone(self, x, y, pieceNo):
        if self.cells[x][y] != BLANK:
            print(XS[x] + YS[y] + 'はすでに置かれています')
            return False
        if self.putInfo[x][y] == None:
            print(XS[x] + YS[y] + 'は置けません')
            return False
        flippable = self.putInfo[x][y]['flippable']
        # 実際に石を置く処理
        self.cells[x][y] = self.current
        self.previousCnt = self.putInfo[x][y]['cnt'] 
        if self.current == self.player:
            self.boardPiece[x][y] = pieceNo # 置いた駒を覚えておく
        for xf, yf in flippable:
            self.cells[xf][yf] = self.current
            self.boardPiece[xf][yf] = None # 返したら駒を消す
        self.current *= -1
        self.allCheckStone()
        return True

    # 盤面確認用(自動計算には必要なし)
    def boardPrint(self):
        if self.current == BLACK:
            print('黒番')
        else:
            print('白番')
        stone = ['〇', '*', '●']
        bord =['', '', '', '', '', '']
        for cell in self.cells:
            for y, i in enumerate(cell):
                bord[y] += stone[i+1]
        for s in bord:
            print(s)
        for x, y in self.puts:
            info = self.putInfo[x][y]
            comb = ''
            if len(self.putInfo[x][y]['combos']) > 0:
                for no in self.putInfo[x][y]['combos']:
                    comb += DECK[no]['name'] + ' '
                comb += 'とコンボ!!'
            print(XS[x] + YS[y] + 'は' + str(info['cnt']) + '枚返し ' + comb)

    # スキル(コンボ含)の倍率計算
    def getSkillMultiply(self, info, skill):
        type = skill.type
        if type == "aura":
            return 0
        elif type == "fixed":
            return  skill.max
        elif type == "whisper":
            one = Decimal(skill.one) ** (len([i for j in self.boardPiece for i in j if i]) + 1)
            if skill.max <= one:
                return skill.max 
            return one
        elif type == "my_board":
            if len([i for j in self.cells for i in j if i == self.player]) <= skill.premise:
                return skill.max 
        elif type == "mai":
            if info['cnt'] >= skill.premise:
                return skill.max 
        elif type == 'previous':
            if self.previousCnt == skill.premise:
                return skill.max 
        return 0

    # 指定駒での倍率計算
    def getMultiply(self, info, pieceNo):
        multiply = Decimal('1')
        if info['cnt'] != 1:
            multiply = Decimal('1.2') ** (info['cnt'] - 1)
        skillMultiply = self.getSkillMultiply(info, DECK[pieceNo]['skill'])
        if skillMultiply == 0:
            return multiply
        comboMultiply = 1
        for no in info['combos']:
            cm = self.getSkillMultiply(info, DECK[no]['combo'])
            if cm != 0:
                comboMultiply *= cm
        return multiply * skillMultiply * comboMultiply

    # 各駒を置いた時の情報を表示
    def infoDisplay(self):
        for i, no in enumerate(self.tegomas):
            print(DECK[no]['name'] + ' ATK: ' + str(self.atks[i]))
            for x, y in self.puts:
                print(XS[x] + YS[y] + '  ' + str(self.atks[i] * self.getMultiply(self.putInfo[x][y], no)))

少し長めのプログラムになってしまいましたが、特別なことをしているわけではないのでまとめて今回の記事にしてみました。

逆転オセロニアのダメージ計算で使うスキルやコンボを定義するための「Skill」クラスと、逆転オセロニアのダメージ計算用「Damellonia」クラスになります。

逆転オセロニアにはキャラ駒、返した枚数、スキル、コンボという概念もあるので置いた場合の情報も一部退避させています。

コメントも多めに書いたのでプログラムを見ると、ある程度分かると思います。

スキル(コンボ)定義用クラス

# スキル(コンボ)定義用クラス
class Skill:
    def __init__(self, _type, **mapping):
        self.type = _type
        if "one" in mapping: self.one = Decimal(mapping['one'])
        if "max" in mapping: self.max = Decimal(mapping['max'])
        if "premise" in mapping: self.premise = mapping['premise']

「Skill」クラスは必要か少し悩みましたが、後からプログラムを見た際に、何のプロパティを定義したのかわからなくなってきたので、プロパティ専用のクラスを別で作っておきました。

スキルの発動条件については一部処理を省いています。(竜単のみの発動条件など)

スキルの種類を増やしたい場合、ここに発動条件や判断に使用する値などのプロパティを追加するといいと思います。

デッキについて

ダメージ計算用デッキ.png

今回は記事のため、比較的楽に実装できそうなスキル・コンボを持つ駒に限定しています。

DECK = {'0751': {'name': 'デネブ', 'skill': Skill('aura'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1926': {'name': '竜フェリヤ', 'skill': Skill('fixed', max='2'), 'combo': Skill('fixed', max='2.2')},
        '1719': {'name': 'ジェンイー', 'skill': Skill('my_board', premise=12, max='1.9'), 'combo': Skill('whisper', one='1.3', max='2.2')},
        '3414': {'name': 'ルーシュ', 'skill': Skill('whisper', one='1.3', max='2'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '3133': {'name': 'ランメリー', 'skill': Skill('fixed', max='1.9'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '3001': {'name': '京極', 'skill':Skill('fixed', max='1.8'), 'combo': Skill('whisper', one='1.3', max='1.6')},
        '2968': {'name': 'イモ', 'skill': Skill('aura'), 'combo': Skill('mai', premise=2, max='1.6')},
        '2250': {'name': '雛クロリス', 'skill': Skill('fixed', max='2.1'), 'combo': Skill('mai', premise=2, max='1.6')},
        '2206': {'name': 'ラウラ', 'skill': Skill('whisper', one='1.3', max='1.9'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '1933': {'name': '竜守護者', 'skill': Skill('fixed', max='1.7'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1892': {'name': 'エルツドラッヘ', 'skill': Skill('my_board', premise=8, max='1.7'), 'combo': Skill('whisper', one='1.3', max='2')},
        '1646': {'name': '呂蒙', 'skill': Skill('previous', premise=1, max='1.3'), 'combo': Skill('whisper', one='1.3', max='1.8')},
        '1436': {'name': 'グレリオ', 'skill': Skill('fixed', max='1.5'), 'combo': Skill('fixed', max='1.6')},
        '0848': {'name': 'ファイドレ', 'skill': Skill('mai', premise=2, max='1.8'), 'combo': Skill('whisper', one='1.3', max='2')},
        '0847': {'name': 'ランタイ', 'skill': Skill('aura'), 'combo': Skill('fixed', max='1.4')},
        '0755': {'name': 'クロリス', 'skill': Skill('fixed', max='1.8'), 'combo': Skill('fixed', max='1.6')}}

「DECK」 という定数に使用しているデッキの情報を定義しています。
キー項目になっている数値は一意になるように図鑑番号を使用しています。

今回は相手の駒は考慮しないので相手用のデッキ情報は持たせていません。
相手の防御スキルの計算なども行いたいのであれば、相手用のデッキも作っていいかもしれませんね。

ダメージ計算用クラスのプロパティ値

        self.cells = [[0, 0, 0, 0, 0, 0],
                     [0, 0, 0, 0, 0, 0],
                     [0, 0,-1, 1, 0, 0],
                     [0, 0, 1,-1, 0, 0],
                     [0, 0, 0, 0, 0, 0],
                     [0, 0, 0, 0, 0, 0]] # 現在の盤面の状況
        self.current = BLACK # 現在の順番
        self.player = my_stone # 自分の石の色
        self.boardPiece = [] # 盤面の自分の駒
        self.puts = []   # 現在置ける位置
        self.putInfo = []   # 置いた場合の情報
        self.previousCnt = 0   # 直前に返された枚数
        self.atks = [None,None,None,None] # 画面から識別したATKの値
        self.tegomas = [None,None,None,None] # 画面から識別した手駒番号

現在の盤面状況や、次に置ける個所などダメージ計算に必要な情報をプロパティに持たせています。

通常のオセロ部分

「Damellonia」クラスを実行すると盤面上の置ける位置の確認「allCheckStone」を自動で行うので、石を置く「putStone」を実行して「boardPrint」で盤面を確認するという流れになります。

通常のオセロと違う部分

盤面上の置ける位置の確認するときに、置ける枚数や盤面の情報などを「putInfo」に退避させたり、他にもダメージ計算に必要な情報を退避したりしています。

自動監視で「atks」や「tegomas」に情報が入れば「infoDisplay」を呼び出すと手駒を置いた時のダメージを計算し表示を行うという仕組みですね。

今回の記事では、自動監視ロジックなどは入れていないので、自分で手駒内容(「atks」「tegomas」)を設定し、駒を置く位置を指定したりすると動作確認ができます。

動作確認

それでは動作確認をしてみます。

初期表示

ダメージ計算オセロニア画面1.png

この情報を手動入力して動作確認を行ってみます。

if __name__ == '__main__':
    damellonia = Damellonia()
    # damellonia = Damellonia(WHITE) # 白番にしたい場合は引数にWHITEを指定
    damellonia.atks = [2121, 1838, 2590, 1693] # 自動でATKが設定される想定
    damellonia.tegomas = ['0751', '1646', '1926', '0848'] # 自動で手駒番号が設定される想定
    damellonia.infoDisplay() # 各駒を置いた時の情報を表示

を実行すると

ダメージ計算Jupyter画面1.png

最初は黒番で「B3, C2, D5, E4」に配置可能でどこに置いても1枚返しですね。

デネブはオーラスキルだからATKのまま

呂蒙はスキルが1.3倍だけど直前に1枚返しの条件があるからスキル発動なしでATKのまま

竜フェリヤは2.0倍固定だからATK 2590*2.0=5180

ファイドレは2枚返し以上で1.8倍固定だから1枚返しでスキル発動なしでATKのまま

なので合っていそうな感じですね。

黒番 初手D5に駒を配置

ダメージ計算オセロニア画面2.png

ファイドレのD5が1693ダメージなので合っていますね

すると相手の白番がE5にX打ちをしてきました。

ダメージ計算オセロニア画面3.png

この段階の情報も追加で手動入力して動作確認を行ってみます。

    damellonia.putStone(3, 4, '0848') # 3, 4 はD5の事 '0848'はファイドレの図鑑番号
    damellonia.putStone(4, 4, None) # 4, 4 は E5 の事 相手の番なので駒番号はNone
    damellonia.atks = [2121, 1838, 2590, 2075] # 自動でATKが設定される想定
    damellonia.tegomas = ['0751', '1646', '1926', '3414'] # 自動で手駒番号が設定される想定
    damellonia.boardPrint() # 盤面情報を表示
    damellonia.infoDisplay() # 各駒を置いた時の情報を表示

ダメージ計算Jupyter画面2.png

次の黒番は「B3, C2, E4, F5」に置けますね。

さらにF5だと初手に置いたファイドレとコンボするみたいですね。

デネブはオーラスキルだからATKのまま

呂蒙はスキルが1.3倍で直前に1枚返しなのでATK 1838*1.3=2389
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 1838*1.3*1.3*1.3=4038

竜フェリヤは2.0倍固定だからATK 2590*2.0=5180
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 2590*2.0*1.3*1.3=8754

ルーシュは1.3囁きだからATK 2075*1.3*1.3=3506
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 2075*1.3*1.3*1.3*1.3=5926

になるみたいですね。

続けて黒番 C2に駒を配置

ダメージ計算オセロニア画面4.png

呂蒙をC2に置いたので2389ダメージですね。

すると相手はさらにB2にX打ちしてきました。

ダメージ計算オセロニア画面5.png

この段階の情報も追加で手動入力して動作確認を行ってみます。

    damellonia.putStone(2, 1, '1646') # 2, 1 はC2の事 '1646'は呂蒙の図鑑番号
    damellonia.putStone(1, 1, None) # 1, 1 は B2 の事 相手の番なので駒番号はNone
    damellonia.atks = [2757, 2717, 3367, 2697] # 自動でATKが設定される想定
    damellonia.tegomas = ['0751', '3133', '1926', '3414'] # 自動で手駒番号が設定される想定
    damellonia.boardPrint() # 盤面情報を表示
    damellonia.infoDisplay() # 各駒を置いた時の情報を表示

ダメージ計算Jupyter画面3.png

次の黒番は「A2, B3, E4, F5」に置けますね。

さらにA2だと2手目に置いた呂蒙とコンボするみたいですね。
さらにF5だと初手に置いたファイドレとコンボするみたいですね。

デネブはオーラスキルだからATKのまま

ランメリーはスキルが1.9倍固定なのでATK 2717*1.9=5162
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 2717*1.9*1.8=9292
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 2717*1.9*2.0=10324

竜フェリヤは2.0倍固定だからATK 3367*2.0=6734
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 3367*2.0*1.8=12121
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 3367*2.0*2.0=13468

ルーシュは1.3囁きでmaxの2.0だからATK 2697*2.0=5394
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 2697*2.0*1.8=9709
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 2697*2.0*2.0=10788

になるみたいですね。

続けて黒番 F5に駒を配置コンボ発動

ダメージ計算オセロニア画面6.png

竜フェリヤをF5に置いたのでファイドレとコンボして13468ダメージで合っていますね!!

2枚返し以上が出てきませんでしたが動作確認は終わります(´Д`)

まとめ

今回はここまでにします。

逆転オセロニアのメインプログラムと思われる、オセロ部分とダメージ計算のロジックは200行未満のプログラムで出来ていますね(´・ω・`)

少し手を加えればGUI付きのオセロニアも実装できますね。
ちなみに以前作成した時の動画に出ているGUIはTkinterを使用しています。

今までの記事を組み合わせるだけで「逆転オセロニア」の自動ダメージ計算はできますよね(●´ω`●)

ここで終わってもいい気もしますが、気力があれば続きも書いていきたいと思います。

2
0
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
2
0