逆転オセロニア画面を解析して自動でダメージ計算
逆転オセロニアとはオセロのルールで駒を置いていき、スキルやコンボを駆使して相手のHPを削ったら勝ちという対戦型のスマホゲームアプリになります。
そのためダメージ計算はかなり重要な部分になってきます。
詳しくは
で確認してみてください。
その逆転オセロニアのダメージ計算を自動で行うためのプログラムを1年ほど前に作成していましたが、その時の作業手順を思い出しながら少しだけ解説していこうと思います。
--- ## 前回までの記事オセロニアの画面を解析してダメージ計算できるプログラムを作ってみました(●´ω`●)
— とよとよ (@toyotoyo_) October 25, 2018
端数の計算が間違ってるなど、まだまだ問題だらけです(@_@;) pic.twitter.com/WL7FaiBmz3
「逆転オセロニア」を画像解析して自動ダメージ計算 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」クラスは必要か少し悩みましたが、後からプログラムを見た際に、何のプロパティを定義したのかわからなくなってきたので、プロパティ専用のクラスを別で作っておきました。
スキルの発動条件については一部処理を省いています。(竜単のみの発動条件など)
スキルの種類を増やしたい場合、ここに発動条件や判断に使用する値などのプロパティを追加するといいと思います。
デッキについて
今回は記事のため、比較的楽に実装できそうなスキル・コンボを持つ駒に限定しています。
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」)を設定し、駒を置く位置を指定したりすると動作確認ができます。
動作確認
それでは動作確認をしてみます。
初期表示
この情報を手動入力して動作確認を行ってみます。
if __name__ == '__main__':
damellonia = Damellonia()
# damellonia = Damellonia(WHITE) # 白番にしたい場合は引数にWHITEを指定
damellonia.atks = [2121, 1838, 2590, 1693] # 自動でATKが設定される想定
damellonia.tegomas = ['0751', '1646', '1926', '0848'] # 自動で手駒番号が設定される想定
damellonia.infoDisplay() # 各駒を置いた時の情報を表示
を実行すると
最初は黒番で「B3, C2, D5, E4」に配置可能でどこに置いても1枚返しですね。
デネブはオーラスキルだからATKのまま
呂蒙はスキルが1.3倍だけど直前に1枚返しの条件があるからスキル発動なしでATKのまま
竜フェリヤは2.0倍固定だからATK 2590*2.0=5180
ファイドレは2枚返し以上で1.8倍固定だから1枚返しでスキル発動なしでATKのまま
なので合っていそうな感じですね。
黒番 初手D5に駒を配置
ファイドレのD5が1693ダメージなので合っていますね
すると相手の白番がE5にX打ちをしてきました。
この段階の情報も追加で手動入力して動作確認を行ってみます。
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() # 各駒を置いた時の情報を表示
次の黒番は「B3, C2, E4, F5」に置けますね。
さらにF5だと初手に置いたファイドレとコンボするみたいですね。
デネブはオーラスキルだからATKのまま
呂蒙はスキルが1.3倍で直前に1枚返しなのでATK 18381.3=2389
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 18381.31.31.3=4038
竜フェリヤは2.0倍固定だからATK 25902.0=5180
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 25902.01.31.3=8754
ルーシュは1.3囁きだからATK 20751.31.3=3506
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁き)が加算されるので、ATK 20751.31.31.31.3=5926
になるみたいですね。
続けて黒番 C2に駒を配置
呂蒙をC2に置いたので2389ダメージですね。
すると相手はさらにB2にX打ちしてきました。
この段階の情報も追加で手動入力して動作確認を行ってみます。
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() # 各駒を置いた時の情報を表示
次の黒番は「A2, B3, E4, F5」に置けますね。
さらにA2だと2手目に置いた呂蒙とコンボするみたいですね。
さらにF5だと初手に置いたファイドレとコンボするみたいですね。
デネブはオーラスキルだからATKのまま
ランメリーはスキルが1.9倍固定なのでATK 27171.9=5162
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 27171.91.8=9292
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 27171.9*2.0=10324
竜フェリヤは2.0倍固定だからATK 33672.0=6734
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 33672.01.8=12121
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 33672.0*2.0=13468
ルーシュは1.3囁きでmaxの2.0だからATK 26972.0=5394
さらに呂蒙とコンボすると呂蒙のコンボ(1.3の囁きでmaxの1.8)が加算されるので、ATK 26972.01.8=9709
さらにファイドレとコンボするとファイドレのコンボ(1.3の囁きでmaxの2.0)が加算されるので、ATK 26972.0*2.0=10788
になるみたいですね。
続けて黒番 F5に駒を配置コンボ発動
竜フェリヤをF5に置いたのでファイドレとコンボして13468ダメージで合っていますね!!
2枚返し以上が出てきませんでしたが動作確認は終わります(´Д`)
まとめ
今回はここまでにします。
逆転オセロニアのメインプログラムと思われる、オセロ部分とダメージ計算のロジックは200行未満のプログラムで出来ていますね(´・ω・`)
少し手を加えればGUI付きのオセロニアも実装できますね。
ちなみに以前作成した時の動画に出ているGUIはTkinterを使用しています。
今までの記事を組み合わせるだけで「逆転オセロニア」の自動ダメージ計算はできますよね(●´ω`●)
ここで終わってもいい気もしますが、気力があれば続きも書いていきたいと思います。