できるだけChatGPTだけで作るリバーシ(オセロ)ソフト その6(半分自前)

CPUらしく定石を設定しよう

強くするにはどうすればいいかをいくつか考えていましたが、まずは定石を入れようという結論に達しました。

で、ChatGPTに聞いてみたところ、以下のような結果に。

リバーシ(オセロ)の定石をプログラムに組み込むには、まず何を目指すかを明確にすることが大切です。多くの場合、定石とは特定の盤面状況においてベストとされる手順を示すものであり、これをプログラムに組み込むことでAIの強化を図ることができます。

以下に、リバーシの定石を定義する基本的なステップを示します。

1. **定石データの収集**
- 専門書、ウェブサイト、ゲームの解析などから、定石として認識されている手順を収集します。

2. **定石のデータ構造を定義**
- 定石をプログラムに組み込むためのデータ構造を定義します。例えば、定石を連続した手順のリストとして定義することが考えられます。

3. **盤面の評価**
- 盤面の状抶況を評価する関数を定義します。これにより、現在の盤面が定石のどの状況に該当するかを判定することができます。

4. **定石に基づく手を選択**
- 現在の盤面が定石に該当する場合、次に打つ手を定石から選択します。

例として、簡単な序盤の定石を定義してみましょう:

```python
class Reversi:
def __init__(self):
self.BOARD_SIZE = 8
self.board = [[0 for _ in range(self.BOARD_SIZE)] for _ in range(self.BOARD_SIZE)]

# 定石データの例 (盤面, 手順)
self.opening_book = [
([
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 1, 0, 0, 0],
[0, 0, 0, 1, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]
], (2, 3))
]

def get_opening_move(self):
for opening_position, move in self.opening_book:
if self.board == opening_position:
return move
return None

game = Reversi()
print(game.get_opening_move()) # (2, 3) と出力される
```

この例では、ある盤面状況に対してのベストな手を定義しています。実際のゲームでの利用を考えると、もちろんもっと多くの定石データが必要ですが、基本的なアイディアはこのような感じです。

希望する定石の数や詳細に応じて、上記の方法を拡張することで実装を進めることができます。

たぶん、この辺りは自前である程度準備しないと無理そうだという結論になりました。

で、今回は序盤定石に関してサイトの情報を基にChatGPTにプログラムを書いてもらおうという方向に。

今回参考にしたのは下記のサイト

ここに書いてある「F5D6~」は将棋でいうところの棋譜で、横軸をa~h、縦軸を1~8として置く石の順番を示しています。
この手順をテキストファイルやDBに登録して、盤面から適用するものを読み出すことで序盤は思考せずに強くできるという寸法です。

データの保持方法

今回はあまり深いことは考えず、わかりやすいようにDBに保持しようと思います。
テーブルとしては、盤面の状態、次に打つ手を主キーとしたテーブルでいいかなと。

まず、盤面の状態。
ここまで書いてきた(ChatGPTに書かせてきた)プログラムでは、盤面全部の配列を持っています。
これをハッシュ化することで、1つの文字列として保持できるので、検索しやすいようにできます。

そして、分岐がある次の手を第2主キーとしてやることで一意の情報ができます。
黒白どちらかなのかは盤面から把握できますが、念のため項目に持たせてもいいかもしれません。

登録するものは正式名称があるものを登録しておくといいでしょう。

探索関数の修正

DBに登録した定石情報がある場合、それをさいようします。複数あればランダムで選ぶようにしましょう。
同一盤面がなくなったところから、これまでの探索関数を使うという形にすると、序盤はほぼノータイムで指すことができるはずです。

ここまでの話をChatGPTで確認する

ここまでの話をChatGPTに投げて、テーブル定義、ハッシュ化、検索などを行っていきます。
最終的に作成したのは、これ。
テーブル定義だけはこちらで行いました。

90度、270度とありますが、実際には斜め対角線上に折り込んだ位置です(最初はこれでめちゃくちゃ苦労した)。
ここの部分を作ってもらうのにとにかく時間がかかったため、なかなか公開できない状態でした。
これをやっておかないと、初手4か所どこに置いても同じ定石になるようにできないためです。

import sqlite3

BOARD_SIZE = 8

def translate_coordinate_pair(coordinate_pair):
    x_mapping = {'A': '0', 'B': '1', 'C': '2', 'D': '3',
                 'E': '4', 'F': '5', 'G': '6', 'H': '7'}
    y_mapping = {'1': '0', '2': '1', '3': '2', '4': '3',
                 '5': '4', '6': '5', '7': '6', '8': '7'}

    coordinate_pair = coordinate_pair.upper()
    return x_mapping[coordinate_pair[0]] + y_mapping[coordinate_pair[1]]

def rotate_180(coord):
    x, y = int(coord[0]), int(coord[1])

    # 180度回転の定義(変更なし)
    new_x = 7 - x
    new_y = 7 - y

    return str(new_x) + str(new_y)

def fixed_rotate_90(coord):
    x, y = int(coord[0]), int(coord[1])
    new_x = y
    new_y = x
    return str(new_x) + str(new_y)

def fixed_rotate_270(coord):
    x, y = int(coord[0]), int(coord[1])
    new_x = 7 - y
    new_y = 7 - x
    return str(new_x) + str(new_y)

def process_line_swapped(line):
    n = len(line)
    if n % 2 != 0:
        raise ValueError("Line length should be even.")
    
    translated_pairs = [translate_coordinate_pair(line[i:i+2]) for i in range(0, n, 2)]
    rotated_90_pairs = [fixed_rotate_90(pair) for pair in translated_pairs]
    rotated_180_pairs = [rotate_180(pair) for pair in translated_pairs]
    rotated_270_pairs = [fixed_rotate_270(pair) for pair in translated_pairs]

    return "".join(translated_pairs), "".join(rotated_90_pairs), "".join(rotated_180_pairs), "".join(rotated_270_pairs)

def coordinates_to_move_string(coordinates):
    """
    Convert a coordinate string like '5435' to a move string like 'F5D6'.

    Args:
    - coordinates: a string of concatenated x,y coordinates like '5435'.

    Returns:
    - A string representing the moves like 'F5D6'.
    """
    moves = []
    for i in range(0, len(coordinates), 2):
        x = coordinates[i]
        y = coordinates[i+1]
        # Convert to ASCII characters
        column = chr(int(x) + ord('A'))
        row = str(int(y) + 1)
        moves.append(column + row)
    return ''.join(moves)

def to_bitboards(board):
    white_bitboard = 0
    black_bitboard = 0
    
    for i in range(BOARD_SIZE):
        for j in range(BOARD_SIZE):
            position = i * BOARD_SIZE + j
            
            if board[i][j] == -1:  # If it's a white piece
                white_bitboard |= (1 << position)
            elif board[i][j] == 1:  # If it's a black piece
                black_bitboard |= (1 << position)
    
    return white_bitboard, black_bitboard

def process_file(input_file):
    conn = sqlite3.connect("othello_data.db")
    c = conn.cursor()
    
    with open(input_file, 'r') as infile:
        board = init_board()

        for line in infile:
            line = line.strip()
            translated, rotated_90, rotated_180, rotated_270 = process_line_swapped(line)
            trn = coordinates_to_move_string(translated)
            r90 = coordinates_to_move_string(rotated_90)
            r180 = coordinates_to_move_string(rotated_180)
            r270 = coordinates_to_move_string(rotated_270)

            #print(f"Translated: {trn}") 
            #print(f"Rotated 90: {r90}")
            #print(f"Rotated 180: {r180}")
            #print(f"Rotated 270: {r270}")
            
            data = [
                (translated, trn),
                (rotated_90, r90),
                (rotated_180, r180),
                (rotated_270, r270)
            ]

            for moves, rotation in data:
                process_moves(moves, rotation, c)
            
    conn.commit()
    conn.close()

def process_moves(moves, rotation, cursor):
    idx = 0
    board = init_board()
    player = 1
    while idx < len(moves):
        black, white = to_bitboards(board)
        if black == 0:
            print(board)
        x = moves[idx:idx+1]
        y = moves[idx+1:idx+2]
        make_move(board, int(x), int(y), player)
        
        cursor.execute("INSERT OR IGNORE INTO jouseki VALUES (?, ?, ?)", (black, white, rotation[idx:idx+2]))
        idx += 2
        player *= -1
                                                
def init_board():
    board = [[0 for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
    board[3][3] = -1
    board[4][4] = -1
    board[3][4] = 1
    board[4][3] = 1
    return board

def make_move(board, x, y, player):
    if place_piece(board, x, y, player, True):
        player *= -1
        return True
    return False

def place_piece(board, x, y, player, actual=False):
    # 指定した座標に石を置くためのメソッド
    # x, y: 石を置く座標
    # player: 現在のプレイヤー (1 または -1)
    # actual: 実際に石を置くかどうかを示すフラグ
    
    if x < 0 or x >= BOARD_SIZE or y < 0 or y >= BOARD_SIZE or board[x][y] != 0:
        return False
    
    to_flip = []
    DIRECTIONS = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]

    for dx, dy in DIRECTIONS:
        i, j, flips = x + dx, y + dy, []
        while 0 <= i < BOARD_SIZE and 0 <= j < BOARD_SIZE and board[i][j] == -player:
            flips.append((i, j))
            i += dx
            j += dy
        if 0 <= i < BOARD_SIZE and 0 <= j < BOARD_SIZE and board[i][j] == player and flips:
            to_flip.extend(flips)

    if not to_flip:
        return False

    if actual:
        board[x][y] = player
        for i, j in to_flip:
            board[i][j] = player
    return True



if __name__ == '__main__':
    input_filename = "joseki.txt"
    process_file(input_filename)

読み込ませるファイルは

F5D6
F5D6C3
F5D6C3D3C4
F5D6C3D3C4B5
F5D6C3D3C4B3
F5D6C3D3C4B3D2F4
F5D6C3D3C4B3C5
F5D6C3D3C4F4E3
F5D6C3D3C4F4E3F3E6B4
F5D6C3D3C4F4E3F3E6C6

といった感じで、先に紹介したサイトにあるものをテキストエディタなど駆使してシンプルにしたものです。

同じような感じで対局棋譜も読ませてDBに出力させておけば、使えるかなという感じです。

めちゃくちゃ引っ張りましたが、ここからは自前で組み込みながら作っていくことになるので、ひとまずChatGPTだけでのプログラムはいったんここまで。
あとは、この出力したものを利用して序盤定石、中盤の同一局面対応などを組み込んでいけば、そこそこの強さのものができるんじゃないかなと思っています。

ただ、機械学習はここまでが準備。ここから推論で次の1手を打てるかだと思っています。ボチボチ時間を作って自分なりのものを作ろうと思います。

この記事が気に入ったらサポートをしてみませんか?