六角形マップ(HEX)のプログラミング【解説編】

Python
Sponsored

この記事では、六角形マップ(HEX)のプログラミング【実例編】で提示したコードの解説を通して、六角形マップ(HEX)のプログラミングについての考え方について述べる。

2次元配列によるHEXの表現

今回のコードでは、六角形マップを表現するために2次元配列を利用するという方法を用いた。

import random

fishes = [1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          1,1,1,1,1,1,1,1,1,1,
          2,2,2,2,2,2,2,2,2,2,
          2,2,2,2,2,2,2,2,2,2,
          3,3,3,3,3,3,3,3,3,3]

f = random.sample(fishes, len(fishes))

ice = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
       [0,f[0], f[1], f[2], f[3], f[4], f[5], f[6], 0, 0, 0, 0, 0],
       [0,f[7], f[8], f[9], f[10], f[11], f[12], f[13], f[14], 0, 0, 0, 0],
       [0, 0, f[15], f[16], f[17], f[18], f[19], f[20], f[21], 0, 0, 0, 0],
       [0, 0, f[22], f[23], f[24], f[25], f[26], f[27], f[28], f[29], 0, 0, 0],
       [0, 0, 0, f[30], f[31], f[32], f[33], f[34], f[35], f[36], 0, 0, 0],
       [0, 0, 0, f[37], f[38], f[39], f[40], f[41], f[42], f[43], f[44], 0, 0],
       [0, 0, 0, 0, f[45], f[46], f[47], f[48], f[49], f[50], f[51], 0, 0],
       [0, 0, 0, 0, f[52], f[53], f[54], f[55], f[56], f[57], f[58], f[59], 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

まず、list変数fishesはHEXの一覧である。魚が1匹のHEXが30個、2匹のHEXが20個、3匹のHEXが10個あり、list変数fにはこのHEXがランダムな順番で格納される。これを、マップデータを表す2次元配列iceの中に配置していく。

iceを整列表示すると上図のようになる。これまでの話より"f[X]"には1~3までの数字が入り、これが各HEXが持つ魚の数を表す。iceの構図としては、魚が1~3匹いる割れていない氷の塊が、魚が0匹の割れた氷に囲まれていることになる(この理由については後述)。

実際の六角形マップに振られた番号との対応関係は以下のとおりである。

次に、2次元配列iceにおける、隣り合った要素について考える。マップ番号44のHEXにを例にとると、

上図の対応関係から、44番のHEXを示す要素は、34, 35, 43, 45, 54, 55番のHEXを示す要素と隣り合っていることがわかる。ここで、2次元配列の要素の座標を (行番号, 列番号) で表すこととすると、 (X, X) の要素が示すHEXは、

 正方向負方向
X軸方向(ヨコ)(X, X+1)(X, X-1)
Y軸方向(タテ)(X+1, X)(X-1, X)
Z軸方向(ナナメ)(X+1, X+1)(X-1, X-1)

の6つの要素が示すHEXと隣り合っていることがわかり、また、(X, X)の要素が示すHEXにいるペンギンは、上記のX, Y, Z軸の3方向に移動することができる。

2次元配列iceの要素

前述のとおり、2次元配列iceの要素はHEXが持つ魚の数を表し、要素が0のときは、そのHEXは「割れた氷」であり、ペンギンがそのHEXへ移動したり、そのHEXを飛び越えて移動したりすることはできないことを表している。また、各プレイヤーのペンギンがいるHEXに対応する要素は負の2桁の値を持ち、例えば以下のような値をとる。

2人対戦時、プレイヤーAの4つのペンギンがいる各HEXの要素 … -10, -11, -12, -13

3人対戦時、プレイヤーCの3つのペンギンがいる各HEXの要素 … -30, -31, -32

移動可能なHEXの判定

turn = i%player_num
can_move = []
move_key = 0
for j in range(6-player_num):
    row = int(str(penguins[turn][j]).zfill(2)[0]) + 1
    column = int(str(penguins[turn][j]).zfill(2)[1]) + int((row+1)/2)
    ice_x = ice[row]
    x_p = [k if k >= 0 else 0 for k in ice_x[column+1:]]
    x_n = [k if k >= 0 else 0 for k in ice_x[:column]][::-1]
    ice_y = [k[column] for k in ice]
    y_p = [k if k >= 0 else 0 for k in ice_y[row+1:]]
    y_n = [k if k >= 0 else 0 for k in ice_y[:row]][::-1]
    ice_z = [ice[k][l] if l >= 0 and l <= 12 else 0 for k, l in enumerate(range(column-row, column-row+10))]
    z_p = [k if k >= 0 else 0 for k in ice_z[row+1:]]
    z_n = [k if k >= 0 else 0 for k in ice_z[:row]][::-1]
    x_can = [(row - 1)*10 + (column - int((row + 1)/2) + k + 1) for k in range(len(x_p[:x_p.index(0)]))] + [(row - 1)*10 + (column - int((row + 1)/2) - k - 1) for k in range(len(x_n[:x_n.index(0)]))]
    y_can = [(row + k)*10 + (column - int((row + k + 2)/2)) for k in range(len(y_p[:y_p.index(0)]))] + [(row - k - 2)*10 + (column - int((row - k)/2)) for k in range(len(y_n[:y_n.index(0)]))]
    z_can = [(row + k)*10 + (column - int((row + k + 2)/2) + k + 1) for k in range(len(z_p[:z_p.index(0)]))] + [(row - k - 2)*10 + (column - int((row - k)/2) -  k - 1) for k in range(len(z_n[:z_n.index(0)]))]
    can_move.append(x_can + y_can + z_can)
    if len(can_move[j]) != 0:
        move_key = 1

上のコードはint変数iを含むfor文の内容からの抜粋である。なお、変数iはゲーム内で経過したターン数となる。int変数turnは0~(プレイヤー数 - 1)の値をとり、どのプレイヤーの手番であるかを示す。list変数can_moveはこの後の移動判定の結果を格納し、最終的に2次元配列となる。int変数move_keyは初期値が0であり、この後の移動判定で手番のプレイヤーがどこかに移動可能なペンギンを1つでも持っていれば1となる。移動判定後もmove_keyの値が0である場合は、そのプレイヤーの手番はスキップされる。

移動判定は、各プレイヤーが持つ各ペンギン毎に行う(for j in range(6-player_num))。なお、int変数player_numはプレイヤーの人数であり、2次元配列penguinsは、penguins[a][b]にa(0~3)番目のプレイヤーのb(0~3)個目のペンギンがいるHEX番号を収納している。

HEX番号に対応する2次元配列の要素

row = int(str(penguins[turn][j]).zfill(2)[0]) + 1
column = int(str(penguins[turn][j]).zfill(2)[1]) + int((row+1)/2)

移動可能なHEXの判定に際し、まずHEX番号に対応する2次元配列iceの要素 (row, column) を計算する。rowはHEX番号の十の位に1を足したものであり、columnは、rowが偶数の時はHEX番号の一の位+row/2、rowが奇数の時はHEX番号の一の位+(row+1)/2である(下図参照)。

なお、2次元配列penguinsには、例えばHEX番号"01"は単に"1"として格納されているため、要素をstr化する際にzfill(2)を用いて十の位を0埋めしている。

44番のHEXを例にとると、row = 5, column = 7であり、以下この場所にいるペンギンの移動可能なHEXの判定について解説する。

移動判定(X, Y, Z軸)

(赤:プレイヤーAのペンギン、青:プレイヤーBのペンギン、黄:プレイヤーCのペンギン、緑:プレイヤーDのペンギン、黒:割れた氷、橙斜線:44番HEXにいるペンギンの移動可能範囲)

移動判定の実際として、上図のような関係が考えられる。ペンギンは他のペンギンか割れた氷に当たるまで無限に進むことができ、このことを実装するためには前述のX, Y, Z軸に移動方向を分解し、さらにそれぞれの軸に沿って正・負の両方向にHEXを1つ1つたどるという手法を用いる。

X軸方向

ice_x = ice[row]
x_p = [k if k >= 0 else 0 for k in ice_x[column+1:]]
x_n = [k if k >= 0 else 0 for k in ice_x[:column]][::-1]
x_can = [(row - 1)*10 + (column - int((row + 1)/2) + k + 1) for k in range(len(x_p[:x_p.index(0)]))] + [(row - 1)*10 + (column - int((row + 1)/2) - k - 1) for k in range(len(x_n[:x_n.index(0)]))]

上のコードでは、下図のような処理を行っている。

まず、44番HEXに対応する2次元配列iceの要素であるice[5][7]のX軸方向に存在する要素をlist変数ice_xに格納する(上図の配列はiceの初期状態を表している。ゲームが進行するにつれて、f[X]だった要素が0や負の値に変化していく)。この場合、ice_xはice[5]に等しい。次に、ice_xのice[5][7]に対応する要素(すなわちice_x[7]、上図の赤色)よりも右に存在する要素をlist変数x_p、左に存在する要素をlist変数x_nに格納し、ice_x[7]に近い順に並べる(すなわち、x_nは逆順にする)。

こうして得られたx_pとx_nのそれぞれについて、要素を0番目から順に調べていく。要素の値が正の場合は、list変数x_canに対応するHEX番号を格納する。この時、前の手順で行ったHEX番号→ (row, column) の変換の逆変換を行う必要がある。

この処理を、値が0または負の要素に出会うまで続けていく。この時、2次元配列iceの端にある要素はすべて0となっているため、必ずこの突き当りに出会って処理は終了する。これが、前述した「割れていない氷の塊が、魚が0匹の割れた氷に囲まれている」ことの理由である。

Y軸方向

ice_y = [k[column] for k in ice]
y_p = [k if k >= 0 else 0 for k in ice_y[row+1:]]
y_n = [k if k >= 0 else 0 for k in ice_y[:row]][::-1]
y_can = [(row + k)*10 + (column - int((row + k + 2)/2)) for k in range(len(y_p[:y_p.index(0)]))] + [(row - k - 2)*10 + (column - int((row - k)/2)) for k in range(len(y_n[:y_n.index(0)]))]

Y軸方向の処理では、ice[5][7]のY軸方向に存在する要素をlist変数ice_yに格納する。この処理は、各行のcolumn(=7)番目の要素をピックアップしていけばよい。以下、X軸と同様にlist変数y_p, y_nを作成し、移動可能なHEX番号をy_canに格納する。

Z軸方向

ice_z = [ice[k][l] if l >= 0 and l <= 12 else 0 for k, l in enumerate(range(column-row, column-row+10))]
z_p = [k if k >= 0 else 0 for k in ice_z[row+1:]]
z_n = [k if k >= 0 else 0 for k in ice_z[:row]][::-1]
z_can = [(row + k)*10 + (column - int((row + k + 2)/2) + k + 1) for k in range(len(z_p[:z_p.index(0)]))] + [(row - k - 2)*10 + (column - int((row - k)/2) -  k - 1) for k in range(len(z_n[:z_n.index(0)]))]

list変数ice_zの取り方は少々特殊であり、上図のようにする。この時、配列外の要素を参照しようとした際には、便宜的に0を格納する。以下、z_p, z_n, z_canのとり方はX軸・Y軸の処理と同様である。

移動可能HEXの保存

can_move.append(x_can + y_can + z_can)
if len(can_move[j]) != 0:
    move_key = 1

以上の処理で得られたX, Y, Z軸それぞれの方向の移動可能なHEXを1つのlistにまとめ、can_moveに格納する。この処理はプレイヤーが持つペンギンの数だけ繰り返されるので、can_moveはペンギンの数に等しいlistを要素に持ち、それぞれのlistの内容はそれぞれのペンギンの移動可能範囲に対応する。また、can_moveに追加したlistが空ではない場合、move_keyの値を1とし、手番のプレイヤーには動かせるペンギンがあることを記録する。

以上で、移動可能なHEXの判定処理は終了である。

まとめ

この記事では実際のコードの一部を解説することによって、六角形マップ(HEX)プログラミングの考え方について示した。このように、六角形マップは2次元配列と対応させて処理を行うことができるが、その際には六角形マップの座標から2次元配列の要素への変換が少々面倒である。

他の方法を知りたい場合は、Hexagonal Gridsが詳しい。六角形マップの実装方法は多彩であり、この記事で紹介した方法が参考になれば幸いであるが、ぜひ自分に合った方法を見つけて欲しいと思う。

Comments