kaggle tutorial Reinforcement Learning part 2

https://www.kaggle.com/code/alexisbcook/one-step-lookahead

の解説です。様々な知識の詰め合わせとなっていますが、英語で丁寧に解説されています。機械翻訳などで読めば意味は理解できるので、code部分の翻訳と解説を行なっていきます。

# 評価関数の実装
```py
# Calculates score if agent drops piece in selected column
def score_move(grid, col, mark, config):
    next_grid = drop_piece(grid, col, mark, config)
    score = get_heuristic(next_grid, mark, config)
    return score

# Helper function for score_move: gets board at next step if agent drops piece in selected column
def drop_piece(grid, col, mark, config):
    next_grid = grid.copy()
    for row in range(config.rows-1, -1, -1):
        if next_grid[row][col] == 0:
            break
    next_grid[row][col] = mark
    return next_grid

# Helper function for score_move: calculates value of heuristic for grid
def get_heuristic(grid, mark, config):
    num_threes = count_windows(grid, 3, mark, config)
    num_fours = count_windows(grid, 4, mark, config)
    num_threes_opp = count_windows(grid, 3, mark%2+1, config)
    score = num_threes - 1e2*num_threes_opp + 1e6*num_fours
    return score

# Helper function for get_heuristic: checks if window satisfies heuristic conditions
def check_window(window, num_discs, piece, config):
    return (window.count(piece) == num_discs and window.count(0) == config.inarow-num_discs)
    
# Helper function for get_heuristic: counts number of windows satisfying specified heuristic conditions
def count_windows(grid, num_discs, piece, config):
    num_windows = 0
    # horizontal
    for row in range(config.rows):
        for col in range(config.columns-(config.inarow-1)):
            window = list(grid[row, col:col+config.inarow])
            if check_window(window, num_discs, piece, config):
                num_windows += 1
    # vertical
    for row in range(config.rows-(config.inarow-1)):
        for col in range(config.columns):
            window = list(grid[row:row+config.inarow, col])
            if check_window(window, num_discs, piece, config):
                num_windows += 1
    # positive diagonal
    for row in range(config.rows-(config.inarow-1)):
        for col in range(config.columns-(config.inarow-1)):
            window = list(grid[range(row, row+config.inarow), range(col, col+config.inarow)])
            if check_window(window, num_discs, piece, config):
                num_windows += 1
    # negative diagonal
    for row in range(config.inarow-1, config.rows):
        for col in range(config.columns-(config.inarow-1)):
            window = list(grid[range(row, row-config.inarow, -1), range(col, col+config.inarow)])
            if check_window(window, num_discs, piece, config):
                num_windows += 1
    return num_windows
```
このcodeは、AIが、ある手を打った場合にどれくらい有利になるかを評価するための、関数を定義しています。これらの関数は、ヒューリスティック（経験則に基づいた評価方法）を用いて、盤面の状態を数値化します。つまり、人間側が中間地点での（仮想の）得点を考え、それが最大になるようにAIが動きます。

- score_move(grid, col, mark, config):
 指定された列にディスクを置いた場合のスコアを計算するメイン関数。
- drop_piece(grid, col, mark, config):
 指定された列にディスクを置いた後の盤面をシミュレーションする補助関数。
- get_heuristic(grid, mark, config): 
盤面の状態に基づいて、ヒューリスティックなスコアを計算する補助関数。
- check_window(window, num_discs, piece, config):
指定された条件を満たすウィンドウがあるかをチェックする補助関数。
- count_windows(grid, num_discs, piece, config):
指定された条件を満たすウィンドウの数を数える補助関数。

ここでウィンドウという表現が突然登場していますが、連続したマスをウィンドウと呼ぶそうです。
```py
def agent(obs, config):
    # 有効な手のリストを取得
    valid_moves = [c for c in range(config.columns) if obs.board[c] == 0]
    # ボードを2Dグリッドに変換
    grid = np.asarray(obs.board).reshape(config.rows, config.columns)
    # 次のターンの各可能なボードにスコアを割り当てるためにヒューリスティックを使用
    scores = dict(zip(valid_moves, [score_move(grid, col, obs.mark, config) for col in valid_moves]))
    # ヒューリスティックを最大化する列（手）のリストを取得
    max_cols = [key for key in scores.keys() if scores[key] == max(scores.values())]
    # 最大化する列からランダムに選択
    return random.choice(max_cols)
```
AIはobsとconfigの2つの引数を受け取るPython関数として実装されます。それぞれコメントを読むと何がしたいかはイメージできそうです、
最高スコアのリストを作成した後に、リストから、ランダムに1つの列を選択しています。これはある程度盤面に偶然性を持たせることが目的でしょう。同じ方法で何度もやられる状態（いわゆるはめ技）になってしまうことを回避しています。