目次

第2章 ルールを実装しよう

2-1. ルールの簡単なおさらい

こちら 日本オセロ連盟競技ルール 等でルールの確認をしても良いですが、ほとんどの人は知っているんじゃないかと思いますので簡単に。

使用する駒は白黒の円盤を重ね合わせてあり、先手は黒の面、後手は白の面を使います。
盤面は8x8のマスでゲーム開始時には中央の4マスに4個の駒を置くので、置けるマスは60箇所になります。

  1. 相手の駒がないマスに自分の駒を置く事ができる。
  2. 自分の駒で空きマスが無い状態で相手の駒を挟む事で、挟まれた駒を裏返し自分の色に変更できる。
  3. 自分の手番で相手の駒を裏返す事ができるマスがある場合、必ず駒を置かねばならない。
  4. 自分の手番で相手の駒を裏返す事ができるマスがない場合、相手の手番になる。
  5. 先手と後手の双方が相手の駒を裏返す事が出来なくなった場合、ゲーム終了になる。
  6. 盤面の駒の色を数え、多い駒色の方、例えば黒が多ければ先手の勝ちとなる。

改めて説明するまでもない感じですが。

ここでは「相手の駒を裏返す」ルールを実装します。手番の入れ替わりやゲーム終了判定は後の章でやりましょう。

2-2. 相手の駒を裏返すとは?

もうわかりきっているとは思いますが確認です。以下は先手と後手の手番が1回ずつ廻った状態の例です。
矢印が指すマスが駒を置けるマスで、各々が水色の矢印で示すマスに自分の駒を置いて相手の駒を裏返して自分の駒の色にしていきます。

自分の駒を置いた場所から縦・横・斜め隣の駒が相手の駒で、1回以上連続して並び自分の駒まで達する場合、その間の駒を裏返して自分の駒の色にすることができます。

もう少し条件をはっきりさせるために、右方向に盤面を辿る事を考えてみます。

これは、相手の駒を裏返す例です。置かれた場所から右方向に辿ります。辿る条件は「隣が相手の駒の時」です。相手の駒だった時はさらにその隣を辿ります。そして自分の駒に辿り着いた時、今度は逆方向に戻りながらマスの駒を裏返して自分の色にしてしまいます。

これは、相手の駒を裏返す事ができない例です。裏返せないという事は駒を置く事ができない、となります。

これは右方向へ辿る例です。実際には左方向、上下方向、斜め方向、計8方向に対して同じ確認を行います。そして、8方向を確認して一切相手の駒を裏返す事ができなかったら、そのマスには駒を置く事ができません。

この確認手順を実装してみましょう。posXとposYに盤面の場所、mycolorに盤面に置く駒、を指定します。

sample100.py
from enum import IntEnum
 
class Cells(IntEnum):
    BLANK      = 0
    BLACK_CHIP = 1
    WHITE_CHIP = 2
    WALL       = 9
 
posX    = 3
posY    = 5
mycolor = Cells.BLACK_CHIP
 
board = [
    [9,9,9,9,9,9,9,9,9,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,1,2,0,0,0,9],
    [9,0,0,0,2,1,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,9,9,9,9,9,9,9,9,9],
]
 
searchVectors = [
    [ 0, -1],  ## upper
    [ 1, -1],  ## upper right
    [ 1,  0],  ## right
    [ 1,  1],  ## lower right
    [ 0,  1],  ## lower
    [-1,  1],  ## lower left
    [-1,  0],  ## left
    [-1, -1]   ## upper left
]
 
 
print(f"put location: {posX}, {posY}")
 
## 8-direction search and piece reverseing process
counter = 0
for sv in searchVectors:
    locatorX = posX + sv[0]
    locatorY = posY + sv[1]
    opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP
 
    ## If the opponent's cells continue
    localCounter = 0
    while board[locatorX][locatorY] == opponent :
        locatorX += sv[0]
        locatorY += sv[1]
        localCounter += 1
 
    ## sandwiched by pieces
    if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) :
        locatorX -= sv[0]
        locatorY -= sv[1]
 
        ## Reverse over your opponent's pieces
        counter += localCounter
        while (locatorX != posX) or (locatorY != posY) :
            board[locatorX][locatorY] = mycolor
            locatorX -= sv[0]
            locatorY -= sv[1]
 
if counter != 0 :
    board[posX][posY] = mycolor
 
## Display game board
print(f"Reversed: {counter}")
for y in range(0, 10):
    for x in range(0, 10):
        if board[x][y] == Cells.BLANK:
            print("・", end="")
 
        elif board[x][y] == Cells.BLACK_CHIP:
            print("●", end="")
 
        elif board[x][y] == Cells.WHITE_CHIP:
            print("○", end="")
 
        elif board[x][y] == Cells.WALL:
            print("■", end="")
    print()

先手(BLACK_CHIP)で

  • posX,posy = 3,5 の時



  • posX,posy = 5,3 の時



  • posX,posy = 4,6 の時



  • posX,posy = 6,4 の時



2-3. 8方向検索

このサンプルでは8方向を検索する為に以下の配列を定義して使っています。

searchVectors = [
    [ 0, -1],  ## upper
    [ 1, -1],  ## upper right
    [ 1,  0],  ## right
    [ 1,  1],  ## lower right
    [ 0,  1],  ## lower
    [-1,  1],  ## lower left
    [-1,  0],  ## left
    [-1, -1]   ## upper left
]

この値は座標に足し込むための増分をx,yの組で持っている配列です。
この増分を盤面の位置(座標)に足し込む事で、指定の方向に盤面上のマスの位置を1マスずつ進めたり、減ずる事で元に戻る事ができるようになります。

例えば盤面のマス(横5,縦3)の位置から、右方向に進めるときは[ 1, 0]の組の値を足し込みます。

盤面X位置 盤面Y位置 進んだ盤面のX位置 進んだ盤面のY位置
5 3 5 + 1 = 6 3 + 0 = 3
6 3 6 + 1 = 7 3 + 0 = 3
7 3 7 + 1 = 8 3 + 0 = 3

例えば盤面のマス(横6,縦4)の位置から、左下方向に進めるときは[-1, 1]の組の値を足し込みます。

盤面X位置 盤面Y位置 進んだ盤面のX位置 進んだ盤面のY位置
6 4 6 + (-1) = 5 4 + 1 = 5
5 5 5 + (-1) = 4 5 + 1 = 6
4 6 4 + (-1) = 3 6 + 1 = 7
3 7 3 + (-1) = 2 7 + 1 = 8

元に戻る時は逆にこの値を減じていきます。posX, posY の値になるまで減じ続ければ元の位置に戻った事になります。

この事を頭に入れてコードを読み進めます。

## 8-direction search and piece reverseing process
counter = 0
for sv in searchVectors:
    locatorX = posX + sv[0]
    locatorY = posY + sv[1]
    opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP
 
    ## If the opponent's cells continue
    localCounter = 0
    while board[locatorX][locatorY] == opponent :
        locatorX += sv[0]
        locatorY += sv[1]
        localCounter += 1
 
    ## sandwiched by pieces
    if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) :
        locatorX -= sv[0]
        locatorY -= sv[1]
 
        ## Reverse over your opponent's pieces
        counter += localCounter
        while (locatorX != posX) or (locatorY != posY) :
            board[locatorX][locatorY] = mycolor
            locatorX -= sv[0]
            locatorY -= sv[1]
 
if counter != 0 :
    board[posX][posY] = mycolor

このfor文は、searchVectorsに定義されている8個の組を一つずつ取り出しています。つまりループで8方向の処理を実施しようとしています。

for sv in searchVectors:
    locatorX = posX + sv[0]
    locatorY = posY + sv[1]
    opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP

指定した盤面の位置にさっそく組の値を足し込んでいます。1つ位置を進めた状態がlocatorX, locatorYに格納されました。

    locatorX = posX + sv[0]
    locatorY = posY + sv[1]

以下は相手の駒を決定しているだけです。
mycolor が自分の駒の色になるので、mycolorが Cells.BLACK_CHIP だったら相手は Cells.WHITE_CHIP、mycolor が Cells.WHITE_CHIP だったら相手は Cells.BLACK_CHIP、になります。

opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP

このwhile文は、相手の駒が隣にある間、locatorX, locatorYを更新し続けます。相手の駒ではない時(自分の駒、空のマス、盤外)にループを抜けます。localCounterは相手の駒の個数になりますね。

    localCounter = 0
    while board[locatorX][locatorY] == opponent :
        locatorX += sv[0]
        locatorY += sv[1]
        localCounter += 1

先のループが終了した時、そこにあるのが自分の駒で、そこに至るまでに相手の駒だけが存在していたのであれば駒の置き直し(裏返し)を行います。
locatorX, locatorYから組の値を減じているのは、現在の位置には自分の駒があるので、その手前、つまり相手の駒の並びの終端に位置付けする為です。

    if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) :
        locatorX -= sv[0]
        locatorY -= sv[1]

ひたすら元の位置に戻りながら駒の置き直し(裏返し)を行います。
locatorX, locatorY が posX, posY と同じ値となったら置き換えは終了です。

        counter += localCounter
        while (locatorX != posX) or (locatorY != posY) :
            board[locatorX][locatorY] = mycolor
            locatorX -= sv[0]
            locatorY -= sv[1]

最後にposX, posYの盤面の位置へ自分の駒を置きます。これで盤面の書換えが完了です。 もし、裏返しが全くできていなければそれは駒を置く事を止めます。 最初に駒を置いてしまったら消すべきかどうか判断ができなくなってしまいます。

if counter != 0 :
    board[posX][posY] = mycolor

2-4. 駒を置くことができるか否か

ルールでは、駒を置けるならどんなにその位置に駒を置くのが不利になる場合でも置かねばならないし、どこにも駒を置くことができなければパスとなり手番が相手に移ります。

つまり、有効な駒の配置可能な位置を手番毎に調べる必要があります。

肝心な調べ方ですが、盤面には最大で64個のマスしかないので、空きマスに実際に駒を置いていくつ裏返す事ができるかを愚直に調べる事にします。
今のコンピュータならこのくらいどうという事はありません。よい時代になったものです。

そして、既にわたしたちは類似の処理を書いています。
指定の盤面のマスの位置を指定して、駒を裏返す処理がそれです。あの処理では、指定の盤面の位置に駒を置いて裏返すと同時に裏返す事ができた駒の個数を返すようになっていました。……駒を裏返す処理をしなければ駒の個数だけを得られる事になりますね。この処理を流用しましょう。

具体的には、この処理を関数化して、引数で駒を裏返す処理をさせるか否か選択できるようにしてしまいましょう。

sample101.py
import random
from enum import IntEnum
 
class Cells(IntEnum):
    BLANK      = 0
    BLACK_CHIP = 1
    WHITE_CHIP = 2
    WALL       = 9
 
board = [
    [9,9,9,9,9,9,9,9,9,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,1,2,0,0,0,9],
    [9,0,0,0,2,1,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,0,0,0,0,0,0,0,0,9],
    [9,9,9,9,9,9,9,9,9,9],
]
 
searchVectors = [
    [ 0, -1],  ## upper
    [ 1, -1],  ## upper right
    [ 1,  0],  ## right
    [ 1,  1],  ## lower right
    [ 0,  1],  ## lower
    [-1,  1],  ## lower left
    [-1,  0],  ## left
    [-1, -1]   ## upper left
]
 
 
def SearchAndReverse(mycolor, posX, posY, revers=False) :
    counter = 0
    for sv in searchVectors:
        locatorX = posX + sv[0]
        locatorY = posY + sv[1]
        opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP
 
        ## If the opponent's cells continue
        localCounter = 0
        while board[locatorX][locatorY] == opponent :
            locatorX += sv[0]
            locatorY += sv[1]
            localCounter += 1
 
        ## sandwiched by pieces
        if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) :
            locatorX -= sv[0]
            locatorY -= sv[1]
 
            ## Reverse over your opponent's pieces
            counter += localCounter
            while (locatorX != posX) or (locatorY != posY) :
                if revers :
                    board[locatorX][locatorY] = mycolor
                locatorX -= sv[0]
                locatorY -= sv[1]
 
    if (counter != 0) and revers :
        board[posX][posY] = mycolor
 
    return counter
 
def CorrectValidPosition(mycolor) :
    poslist=[]
 
    for y in range(1, 8):
        for x in range(1, 8):
            count = SearchAndReverse(mycolor, x, y)
            if count != 0 :
                poslist.append([x, y, count])
    return poslist
 
 
def DisplayBoard():
    for y in range(0, 10):
        for x in range(0, 10):
            if board[x][y] == Cells.BLANK:
                print("・", end="")
 
            elif board[x][y] == Cells.BLACK_CHIP:
                print("●", end="")
 
            elif board[x][y] == Cells.WHITE_CHIP:
                print("○", end="")
 
            elif board[x][y] == Cells.WALL:
                print("■", end="")
        print()
 
 
## demo
 
DisplayBoard()
 
for i in range(1,3):
    print(f"BLACK: search valid position")
    pos = random.choice( CorrectValidPosition(Cells.BLACK_CHIP) )
    print(f"BLACK: put {pos}")
    SearchAndReverse(Cells.BLACK_CHIP, pos[0], pos[1], revers=True)
 
    DisplayBoard()
 
    print(f"WHITE: search valid position")
    pos = random.choice( CorrectValidPosition(Cells.WHITE_CHIP) )
    print(f"WHITE: put {pos}")
    SearchAndReverse(Cells.WHITE_CHIP, pos[0], pos[1], revers=True)
 
    DisplayBoard()

1回目実行

2回目実行

3回目実行


元の処理を関数 SearchAndReverse() にまとめました。

以下の呼び出しで mycolorで示すプレイヤーの置くことが可能な盤面の位置を取得できます。

poslist = SearchAndReverse(mycolor, posX, posY)

別途8×8の盤面の位置を頭から順に調べて駒を置く事が可能な盤面の位置を調べる関数も作りました。

CorrectValidPosition(mycolor)

以下の呼び出しで mycolorで示すプレイヤーの駒を置いて裏返す処理が実行できます。

poslist = SearchAndReverse(mycolor, posX, posY, revers=True)

最後に、盤面をテキスト表示する処理も関数化しました。

DisplayBoard()

関数化したおかげで、デモを作る事が出来ました。このデモで、関数 SearchAndReverse() の振る舞いを確認できます。
駒を置くことができない場合、関数 CorrectValidPosition() の返す配列の要素数がゼロになり、先手後手両者がゼロになればゲーム終了の判定ができるようになります。