コンテンツへスキップ

ポーカーAI開発環境

(最終更新:2025年10月9日)

ポーカーにおけるGTO(Game Theory Optimal)は、ゲーム理論に基づいた戦略であり、「完全な平衡(ナッシュ均衡)」を目指しています。この「完全な平衡」とは、各プレイヤーが自分の戦略を一方的に変更しても利益を得られない状態、すなわち全員が最適な戦略を取っている状態を指します。したがって、GTOとは「相手もGTO戦略を採用している場合における、最も強い戦略」であると言えます。

現実のポーカーにおいて、参加している全てのプレイヤーがGTOにしたがってプレイしている状況なんて聞いたことがないです。人によってゲームの理解度がまちまちですし、ハンドレンジやアグレッションには個性が出ます。従って、GTOが最適解ではない状況はいくらでも発生します。GTOばかり勉強している人がポーカーで結果を出せていない理由はここにあると思います。(ビジネス的な成果は別として)

既存のGTOソルバーよりもパラメータの自由度が高くてより実用的なツールを使ってみたかったのですが、そんなものは存在しなかったので自作を試みました。結論、アプリ化まで開発しきる熱意と時間がなかったですが、開発の過程でゲームの理解度がかなり上がったように感じます。

下記に限らずですが、自分の仮説を検証する形でデータ観測できたのがとても勉強になりました。

  • 卓の半分が3ベット頻度が低く、コールレンジの広いrecの場合、最適な戦略は思ったよりGTOからズレる
  • MCCFRは分散を嫌うため、特にプリフロやフロップあたりは疑似均衡に収束してしまう場合がある(戦略としては弱くないが最適ではない)
  • 学習を回し続けると、新規重要ノードの発掘によって収束方向が大きく変わる場合がある
  • 超低頻度なんてものはなく、収束方向が変わっているのに過去戦略の減衰が不十分なだけ

このページでは、AI開発で自分が構築した環境をメモっておきます。もし試すのであれば、Pythonで要領をつかみ、C++で本格的な開発することをおすすめします。Pythonは処理が遅すぎるので、特にリアルタイム計算に向かないです。

基本はOpen SpielのUniversal Pokerを活用したC++コードをベースにしていますが、以下のような改良を加えたほうがいいです。途中からメモるのをやめたので、他にもコードの工夫の余地はいっぱいあります。

  • Outcome Samplingはsample_reachが0に近く、Cumulative Policyがすぐパンクして均衡へたどり着けないからだめ。External Samplingを利用する。
  • 学習は基本メモリでするため、std::unordered_mapをFacebookのfolly::ConcurrentHashMapにしてスレッドセーフ並列処理を可能にする。
  • 学習データをSSDに保存する際はRocksDBを使い、読み込み時は全体をメモリマッピングをしてから、デシリアライズと.inser()を並列処理する。
  • キーは単なるテキストなため、必要な情報だけに絞って圧縮する。
  • @#$%をスートの代わりにする。ハンドのスートは@#になるようにすれば、どんなスートにでも一般化できる。例:6通りの9ポケは9@9#、4通りのAKスートはA@K@、12通りのAKオフはA@K#で表現でき、ボードも T@4#2$A$6% のようにハンドと比較して表現できる。(後日知ったのですが、このlosslessな抽象化のことをisomorphismsと言うらしいです)
  • プリフロの均衡への収束を早めたい場合は、169通りのハンドをエクイティのアースムーバー距離によって16個くらいのクラスターに分類するといいです。この分野に限らずゲーム抽象化の手法に関しては色んな論文が出てて面白いです。
  • アクションの抽象化は学習結果にどう影響するか慎重にテストしたほうがいいです。例えば急なAll-inができないようにuniversal_poker.ccを変更する。

Ubuntu Serverの初期設定

  1. USBでUbuntu Serverをインストールする。
  2. DNSを8.8.8.8 8.8.4.4に変更する。
  3. ポート開放をする。(例:32199)
  4. ローカルIPアドレスを固定する。(例:192.168.0.99)
  5. NVMeの省電力設定を無効化する。
  6. 普段遣いのノートパソコンからSSH接続できることを確認する。
  7. GPUのドライバーをインストールする。
# 実行してrecommendedと推奨されているドライバーを確認する
ubuntu-drivers devices

# 推奨されているドライバーをインストールする
sudo apt install nvidia-driver-550

# 再起動する
sudo reboot

# ドライバーがインストールされたか確認する
nvidia-smi

Pythonの場合

Jupyter Labの設定

Pythonのコードを書くときはJupyter Labが便利です。

  1. Ubuntuで必要なパッケージをインストールする。
sudo apt install -y git cmake clang build-essential python3 python3-dev python3-pip python3-venv python-is-python3 g++ libboost-all-dev libgmp-dev libsdl2-dev zlib1g-dev tmux
  1. 開発用のPython仮想環境を作成して、その環境に切り替える。
cd ~

python3 -m venv open_spiel_venv

source open_spiel_venv/bin/activate
  1. Jupyter Labをインストールする。
pip install jupyterlab
  1. 設定ファイルを作成する。
jupyter lab --generate-config
  1. 設定ファイルを開き、下記の箇所のコメントアウトを外して編集し、Ctrl+O、Enter、Ctrl+Xで閉じます。
sudo nano ~/.jupyter/jupyter_lab_config.py
c.ServerApp.ip = '0.0.0.0'
c.ServerApp.open_browser = False
c.ServerApp.port = 8888
  1. Jupyter Labにパスワードを設定します。
jupyter lab password
  1. サーバーでJupyter Labを起動します。tmuxでJupyter Lab用のセッションを開始するので、SSHを切ってもJupyter Labは起動し続けることができます。Ctrl+BしてからすぐDを押してtmuxセッションを抜けます。
tmux new -s jupyter

source open_spiel_venv/bin/activate

jupyter lab --no-browser --port=8888

# Jupyter Labのtmuxセッションに戻りたいとき
tmux attach -t jupyter
  1. 普段遣いのノートパソコンの別のターミナルで下記を実行し、サーバーのポート8888とノートパソコンのlocalhostを繋げます。
ssh -L 8888:localhost:8888 -p 32199 username@192.168.0.99
  1. ノートパソコンのブラウザで下記を開き、Jupyter Labのパスワード入力画面が出てくれば設定OKです。
http://localhost:8888

OpenSpielのインストール

Python用の公式インストールガイド

  1. OpenSpielのリポジトリをクローンする。2025年1月時点ではv1.5でした。
cd ~

git clone https://github.com/deepmind/open_spiel.git
  1. Pythonの依存パッケージをインストールする。
cd ~

source open_spiel_venv/bin/activate

cd ~/open_spiel

pip install --upgrade pip

pip install --upgrade setuptools testresources

pip install -r requirements.txt

pip install pandas

pip install matplotlib
  1. 標準だとOpenSpielのビルド時にポーカーが除外されてしまうので、nano ~/open_spiel/open_spiel/CMakeLists.txtnano ~/open_spiel/open_spiel/scripts/global_variables.shでポーカーの箇所をOFFからONにする。
openspiel_optional_dependency(OPEN_SPIEL_BUILD_WITH_ACPC           ON
  "Build against the Universal Poker library.")
export OPEN_SPIEL_BUILD_WITH_ACPC="ON"
  1. OpenSpielをビルドする。
cd ~/open_spiel

./install.sh

./open_spiel/scripts/build_and_run_tests.sh
  1. nano ~/open_spiel_venv/bin/activateで仮想環境の設定を開き、先頭にPATHを追加する。PATHを追加したら、Jupyter Labを含む全ての仮想環境を一度deactivateし、またsource open_spiel_venv/bin/activateで仮想環境を開く。
export PYTHONPATH=~/open_spiel:$PYTHONPATH

export PYTHONPATH=~/open_spiel/build/python:$PYTHONPATH

2回目以降のPython環境起動方法

  1. サーバーにSSHで接続する。Jupyter Labを起動したままであれば4に飛ぶ。
ssh -p 32100 username@192.168.0.91
  1. Jupyter Labのtmuxセッションを開き、Pythonの仮想環境を使ってなかったら使う。
tmux attach -t jupyter

# セッションがない場合は作る
tmux new -s jupyter

source open_spiel_venv/bin/activate
  1. Jupyter Labを起動し、Ctrl+BしてからすぐDを押してtmuxセッションを抜ける。
jupyter lab --no-browser --port=8888
  1. サーバーとのSSHをexitで切ってから、ポートを繋げる。
ssh -L 8888:localhost:8888 -p 32100 username@192.168.0.91
  1. ノートパソコンのブラウザでJupyter Labを開く。
http://localhost:8888
  1. Jupyter Labでノートブックに書いたコードを.pyファイルに出力する際は、サーバーで下記を実行します。
jupyter nbconvert --to script example.ipynb

学習を裏で回す方法

SSH画面でスクリプトを実行するとSSH通信が切れた時に実行が止まってしまうので、長時間学習を回す場合はtmuxのセッションを新しく作ってバックグラウンドで回します。

  1. サーバーにSSHで接続する。
ssh -p 32100 username@192.168.0.91
  1. .pyファイル用のtmuxセッションを作る。
tmux new -s py_run

# 既にあるのであれば
tmux attach -t py_run
  1. Pythonの仮想環境を使う。
source open_spiel_venv/bin/activate
  1. .pyファイルのあるディレクトリに移動して.pyファイルを実行する。
python3 your_script.py
  1. Ctrl+BしてからすぐDを押してtmuxセッションを抜ける。

C++の場合

OpenSpielのインストール

C++ライブラリ用の公式インストールガイド

  1. OpenSpielのリポジトリをクローンする。
cd ~

git clone https://github.com/deepmind/open_spiel.git
  1. 標準だとOpenSpielのビルド時にポーカーが除外されてしまうので、nano ~/open_spiel/open_spiel/CMakeLists.txtnano ~/open_spiel/open_spiel/scripts/global_variables.shでポーカーの箇所をOFFからONにする。
openspiel_optional_dependency(OPEN_SPIEL_BUILD_WITH_ACPC           ON
  "Build against the Universal Poker library.")
export OPEN_SPIEL_BUILD_WITH_ACPC="ON"
  1. 依存パッケージをインストールする。
cd ~/open_spiel

./install.sh
  1. OpenSpielをビルドする。
mkdir ~/open_spiel/build

cd ~/open_spiel/build

BUILD_SHARED_LIB=ON CXX=clang++ cmake -DCMAKE_CXX_STANDARD=17 -DPython3_EXECUTABLE=$(which python3) -DCMAKE_CXX_COMPILER=${CXX} ../open_spiel

make -j$(nproc) open_spiel
  1. ターミナルで、export LD_LIBRARY_PATH="${HOME}/open_spiel/build"でパスを設定する。
    (SSHを切ったらパスはリセットされる)
  2. サンプルのshared_library_example.ccをコンパイルする。
cd ~/open_spiel/open_spiel/examples

clang++ -I${HOME}/open_spiel -I${HOME}/open_spiel/open_spiel/abseil-cpp \
        -std=c++17 -o shared_library_example shared_library_example.cc \
        -L${HOME}/open_spiel/build  -lopen_spiel
  1. コンパイルされたshared_library_exampleが実行できることを確認する。
    実行できなければ、ビルドかコンパイルのどこかで問題がある。
cd ~/open_spiel/open_spiel/examples

./shared_library_example breakthrough
  1. universal_poker_test.ccを作成し、コンパイルする。
universal_poker_test.ccの中身
#include <iostream>
#include <memory>
#include <string>

#include "open_spiel/algorithms/external_sampling_mccfr.h"
#include "open_spiel/algorithms/tabular_exploitability.h"
#include "open_spiel/games/universal_poker/universal_poker.h"
#include "open_spiel/spiel.h"
#include "open_spiel/spiel_utils.h"

constexpr char kCustom3PlayerAcpcGamedef[] = R"""(
# (Empty lines and lines starting with an '#' are all ignored)

GAMEDEF
nolimit
numPlayers = 3
numRounds = 1
numSuits = 2
numRanks = 4
numHoleCards = 1

# Set per player, so 3 total
stack = 15 15 15
blind = 0 1 0

# Set per round
firstPlayer = 3
numBoardCards = 0

END GAMEDEF
)""";

int main(int argc, char** argv) {
  std::string acpc_gamedef = kCustom3PlayerAcpcGamedef;
  int num_iters = 2000;
  int report_every = 500;
  
  std::cout << "Input ACPC gamedef (raw): " << acpc_gamedef << std::endl;

  std::shared_ptr<const open_spiel::Game> game =
      open_spiel::universal_poker::LoadUniversalPokerGameFromACPCGamedef(acpc_gamedef);

  const auto& game_down_cast =
      open_spiel::down_cast<const open_spiel::universal_poker::UniversalPokerGame&>(*game);
  std::cout << "Resulting ACPC gamedef used for universal_poker:\n"
            << game_down_cast.GetACPCGame()->ToString() << std::endl;

  open_spiel::algorithms::ExternalSamplingMCCFRSolver solver(*game);
  std::cerr << "Starting MCCFR on " << game->GetType().short_name << "..."
            << std::endl;

  for (int i = 0; i < num_iters; ++i) {
    solver.RunIteration();
    if (i % report_every == 0 || i == num_iters - 1) {
      double exploitability = open_spiel::algorithms::Exploitability(
          *game, *solver.AveragePolicy());
      std::cerr << "Iteration " << i << " exploitability=" << exploitability
                << std::endl;
    }
  }
  
  return 0;
}
clang++ -I${HOME}/open_spiel -I${HOME}/open_spiel/open_spiel/abseil-cpp \
        -std=c++17 -o universal_poker_test universal_poker_test.cc \
        -L${HOME}/open_spiel/build  -lopen_spiel

# Override Warningは無視してもいい。
# 気になるなら、sudo nano ~/open_spiel/open_spiel/games/universal_poker/universal_poker.hのstd::unique_ptr<State> ResampleFromInfostate(/* parameters */) const;の箇所をconst override;に変更する。
  1. コンパイルされたuniversal_poker_testが実行できることを確認する。
    実行できなければ、ポーカーのゲーム定義あたりに問題がある。
./universal_poker_test

Visual Studio Codeの設定

  1. VSCodeを普段遣いのノートパソコンにインストールする。
  2. VSCodeを開き、拡張機能の「Remote – SSH」をインストールする。
  3. 左側のサイドバーに追加されている「リモートエクスプローラー」を開き、SSHにusername@192.168.0.99 -p 32199のように追加して接続する。
  4. VSCodeでSSH接続している状態でターミナルを開き、必要なパッケージをsudo apt install -y build-essential gdb cmakeでインストールする。
  5. Visual Studio Codeに拡張機能の「C/C++ Extension Pack」をインストールする。
  6. VSCodeのターミナルで開発用のディレクトリをmkdir -p ~/poker-aiで作る。
    ※Gitを使えば開発が少し楽になりますし、ディレクトリは手動で作る必要ないです。
  7. プロジェクトを cd ~/poker-ai && code .で開く。
  8. 上記のuniversal_poker_test.ccをこのディレクトリに配置する。
  9. 左側のサイドバーから「実行とデバッグ」を開き、「launch.jsonファイルを作成します」をクリックし、中身を下記に置き換えて保存する。
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "C++ Debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/${fileBasenameNoExtension}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [
                {
                    "name": "LD_LIBRARY_PATH",
                    "value": "${env:HOME}/open_spiel/build"
                }
            ],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C++ Build"
        }
    ]
}
  1. Ctrl Shift Pを同時に押し、「Tasks: Configure Default Build Task」をクリックし、「テンプレートからtasks.jsonを生成」を選択し、「Others」を選択し、中身を下記に置き換えて保存する。
{
    "version": "2.0.0",
    "tasks": [
      {
        "label": "C++ Build",
        "type": "shell",
        "command": "/usr/bin/clang++",
        "args": [
          "-g",
          "-I${env:HOME}/open_spiel",
          "-I${env:HOME}/open_spiel/open_spiel/abseil-cpp",
          "-std=c++17",
          "${file}",
          "-o",
          "${workspaceFolder}/build/${fileBasenameNoExtension}",
          "-L${env:HOME}/open_spiel/build",
          "-lopen_spiel"
        ],
        "group": {
          "kind": "build",
          "isDefault": true
        },
        "problemMatcher": ["$gcc"],
        "detail": "Building using clang++"
      }
    ]
  }
  1. universal_poker_test.ccのコードの左側にブレークポイント(赤点)を作り、デバグができるか確認する。

follyのインストール

cd ~

git clone https://github.com/facebook/folly

cd folly

sudo ./build/fbcode_builder/getdeps.py install-system-deps --recursive

sudo apt install libssl-dev libgflags-dev libgoogle-glog-dev libfmt-dev -y

mkdir _build && cd _build

cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/folly_install

make -j$(nproc)

sudo make install

Tailscaleの設定

家の外からサーバーにアクセスしたいのであれば

  1. Ubuntu ServerにTailscaleをダウンロードする。
    curl -fsSL https://tailscale.com/install.sh | sh
  2. 認証URLを取得し、ブラウザでログインする。(自分はGitHubアカウントでログイン)
    sudo tailscale up
  3. ノートパソコンにもTailscaleをダウンロードしてインストールする。
    https://tailscale.com/download
  4. ノートパソコンのTailscaleにもログインする。
  5. sudo nano /etc/ssh/sshd_configのAllow UsersにTailscaleのIP帯を追加して、SSHをsudo systemctl restart sshで再起動する。
    AllowUsers *@192.168.0.0/16 *@100.64.0.0/10
  6. Tailscaleで新しく割り振られたUbuntu ServerのIPアドレスでSSH接続する。

RocksDBの設定

  1. Ubuntu Serverにsudo apt install -y librocksdb-devでRocksDBをインストールする。
  2. ビルドする時に-lrocksdb -lpthreadを追加する。

あとはコードのサンプル

学習ループ

void RunSolverIterations(open_spiel::algorithms::ExternalSamplingMCCFRSolver& solver, std::atomic<int>& global_iters, int set_iters, int report_every) {
  while (true) {
    int current_iter = global_iters.fetch_add(1, std::memory_order_relaxed);
    if (current_iter >= set_iters) {
      break;
    }
    solver.RunIteration();
    if (current_iter % report_every == 0) {
      std::cerr << "Iteration " << current_iter << std::endl;
    }
  }
}


int main(int argc, char** argv) {
  
  std::string acpc_gamedef = kCustom6PlayerAcpcGamedef;
  int set_iters = 1000000;
  int report_every = 200000;
  int num_sets = 100;

  int num_threads = std::thread::hardware_concurrency();
  // int num_threads = 1;
  
  for (int i = 0; i < num_sets; ++i) {
    std::shared_ptr<const open_spiel::Game> game = open_spiel::universal_poker::LoadUniversalPokerGameFromACPCGamedef(acpc_gamedef);
    open_spiel::algorithms::ExternalSamplingMCCFRSolver solver(game, "default2_db");

    std::cerr << "\nStarting MCCFR. Set: " << i+1 << std::endl;
    auto start_time = std::chrono::high_resolution_clock::now();

    std::atomic<int> global_iters(0);
    std::vector<std::thread> threads;

    for (int t = 0; t < num_threads; ++t) {
      threads.emplace_back(RunSolverIterations, std::ref(solver), std::ref(global_iters), set_iters, report_every);
    }
    for (auto& thread : threads) {
      thread.join();
    }
    threads.clear();

    auto end_time = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed_seconds = end_time - start_time;
    std::cout << "Time taken to run " << set_iters << " iterations: " 
              << elapsed_seconds.count() << " seconds" << std::endl;

    // Save solver to RockesDB
    solver.SaveSolverToRocksDB();
  }

  return 0;
}

戦略の抽出

#include <chrono>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <thread>
#include <fstream>
#include <atomic>
#include <sys/sysinfo.h>
#include <sys/resource.h>
#include <cstdlib>

#include "utils.h"
#include "open_spiel/spiel.h"
#include "open_spiel/spiel_utils.h"

int main(int argc, char** argv) {
    
    // Check if a command-line argument is provided
    int set_count = 0;
    std::string db_path = "default2_db";
    if (argc > 1) {
        try {
            set_count = std::stoi(argv[1]);
        } catch (const std::exception& e) {
            std::cerr << "Invalid argument for set_count. Using default value: 0\n";
        }
    }
    if (argc > 2) {
        db_path = argv[2];
    }
    
    // Open RocksDB
    rocksdb::DB* db;
    rocksdb::Options options;
    options.create_if_missing = false;
    rocksdb::Status open_status = rocksdb::DB::Open(options, db_path, &db);
    if (!open_status.ok()) {
        std::cerr << "Error opening RocksDB: " << open_status.ToString() << std::endl;
        return 1;
    } else {
        std::cout << "Successfully opened RocksDB: " << db_path << std::endl;
    }
    
    // HashMap to store player files
    std::unordered_map<std::string, std::ofstream> player_files;

    // Process each game state key
    // for (const auto& [game_state_key, action_str] : utils::action_map_vsUTG) {
    for (const auto& [game_state_key, action_str] : utils::action_map) {
        std::string serialized_value;

        // Extract player number (first character in key)
        std::string player_str(1, game_state_key[0]);

        // Extract private hand information
        size_t private_pos = game_state_key.find(",");
        size_t next_comma = game_state_key.find(",", private_pos + 1);
        std::string hand_str = game_state_key.substr(private_pos + 1, next_comma - private_pos - 1);

        // Retrieve value from RocksDB
        rocksdb::Status get_status = db->Get(rocksdb::ReadOptions(), game_state_key, &serialized_value);

        // Deserialize value
        open_spiel::algorithms::CFRInfoStateValues info_state_values = 
            open_spiel::algorithms::CFRInfoStateValues::DeserializeBinary(serialized_value);

        // Get cumulative policy and regrets
        const std::vector<double>& cuml_policy = info_state_values.cumulative_policy;
        const std::vector<double>& cuml_regret = info_state_values.cumulative_regrets;
        const std::vector<open_spiel::Action>& actions = info_state_values.legal_actions;

        // Check if the file for this player exists, if not, create it
        if (player_files.find(player_str) == player_files.end()) {
            std::string file_name = "action_policy_player_" + player_str + "_set_" + std::to_string(set_count) + ".csv";
            player_files[player_str].open(file_name);
            // Write CSV header
            player_files[player_str] << "Player,Hand,Action,CumulativePolicy,CumulativeRegrets\n";
        }

        // Write data to corresponding player's file
        std::ofstream& file = player_files[player_str];
        for (size_t i = 0; i < actions.size(); ++i) {
            file << player_str << "," << hand_str << "," << i << "," 
                << cuml_policy[i] << "," << cuml_regret[i] << "\n";
        }
    }

    // Close all file streams
    for (auto& [player, file] : player_files) {
        file.close();
        std::cout << "CSV output written to action_policy_player_" << player << ".csv" << std::endl;
    }

    // Close the database
    delete db;

  return 0;
}

戦略をハンドレンジに可視化(Python)

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as patches
import glob
import os

# Hand tiers
hand_tiers = {}

# Get all relevant CSV files
files = glob.glob("action_policy_player_*.csv")

for file in files:
    # Extract player name from filename
    player_name = os.path.splitext(os.path.basename(file))[0][-7:]

    # Load CSV file
    df = pd.read_csv(file)

    # Pivot the data to list probabilities horizontally
    df_pivot = df.pivot(index=["Player", "Hand"], columns="Action", values="CumulativePolicy").reset_index()

    # Ensure the action probability columns exist
    action_cols = [col for col in df_pivot.columns if isinstance(col, int)]
    if not action_cols:
        print(f"Error: No action probability columns found in {file}. Skipping...")
        continue

    # Normalize the action probabilities
    df_pivot["prob_sum"] = df_pivot[action_cols].sum(axis=1)
    df_pivot[action_cols] = df_pivot[action_cols].div(df_pivot["prob_sum"], axis=0)

    # Map hand tiers to probabilities
    tier_probabilities = df_pivot.set_index("Hand")[action_cols].to_dict(orient="index")

    # Expand hands using hand_tiers and assign probabilities
    expanded_rows = []
    for hand, tier in hand_tiers.items():
        if tier in tier_probabilities:
            row = {"Hand": hand, **tier_probabilities[tier]}
            expanded_rows.append(row)

    # Create new DataFrame with all hands mapped
    df_expanded = pd.DataFrame(expanded_rows)

    # Save the expanded DataFrame
    # output_file = f"{player_name}_expanded.csv"
    # df_expanded.to_csv(output_file, index=False)

    # Define hand ranks
    ranks = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2']
    num_ranks = len(ranks)

    # Create an empty 13x13 grid for visualization
    fig, ax = plt.subplots(figsize=(10, 10))

    # Define action colors
    action_colors = {
        0: "#5BC0DE",  # Blue (Fold)
        1: "#ffff00",  # Yellow (Call)
        2: "#FF0000",  # Red (Pot-size bet)
        # 3: "#ff7f50",  # Coral (Half-pot bet)
    }

    # Function to get grid position
    def get_grid_position(hand):
        if "s" in hand:
            rank1, rank2 = hand[:1], hand[1:2]
            return ranks.index(rank2), ranks.index(rank1)  # Suited hands in upper triangle
        elif "o" in hand:
            rank1, rank2 = hand[:1], hand[1:2]
            return ranks.index(rank1), ranks.index(rank2)  # Off-suited hands in lower triangle

    # Plot each hand's action probabilities as a mini heatmap
    for _, row in df_expanded.iterrows():
        hand = row["Hand"]
        col_idx, row_idx = get_grid_position(hand)
        
        # Get action probabilities
        probs = [row[col] for col in action_cols]
        
        # Create a small square with 4 subregions (for each action probability)
        for i, (action, color) in enumerate(action_colors.items()):
            prob = probs[i]
            rect = patches.Rectangle(
                (col_idx, num_ranks - row_idx - 1),  # Adjust for top-bottom order
                width=1, height=1,
                facecolor=color, alpha=prob,  # Transparency based on probability
                edgecolor="black"
            )
            ax.add_patch(rect)

    # Adjust layout
    ax.set_xticks(np.arange(num_ranks) + 0.5)
    ax.set_yticks(np.arange(num_ranks) + 0.5)
    ax.set_xticklabels(ranks, fontsize=10)
    ax.xaxis.set_label_position('top')
    ax.xaxis.tick_top()
    ax.set_yticklabels(ranks[::-1], fontsize=10)
    ax.set_xlim([0, num_ranks])
    ax.set_ylim([0, num_ranks])
    ax.set_title(f"Poker Hand Range Table for {player_name} (Action Probability Heatmap)", fontsize=14)

    # Grid settings
    ax.set_xticks(np.arange(num_ranks), minor=True)
    ax.set_yticks(np.arange(num_ranks), minor=True)
    ax.grid(which="minor", color="black", linestyle='-', linewidth=0.5)

    # Save and show the plot
    output_image = f"policy_{player_name}_heatmap.jpg"
    plt.savefig(output_image)
    plt.close()

    print(f"Processed {file} -> {output_image}")

過去戦略の減衰

#include <iostream>
#include <rocksdb/db.h>
#include <rocksdb/options.h>
#include <rocksdb/iterator.h>
#include "custom_outcome_sampling_mccfr.h"

int main() {
    std::string db_path_from = "fixed_open_0_db";
    std::string db_path_to = "default_db";
    double DecayRate = 0.5;
    
    // Open From DB
    rocksdb::DB* db_from;
    rocksdb::Options options;
    options.create_if_missing = false; // Database should already exist.
    rocksdb::Status open_status = rocksdb::DB::Open(options, db_path_from, &db_from);
    if (!open_status.ok()) {
        std::cerr << "Error opening RocksDB: " << open_status.ToString() << std::endl;
        return 1;
    } 
    std::cout << "Successfully opened RocksDB: " << db_path_from << std::endl;

    // Open To DB
    rocksdb::DB* db_to;
    options.create_if_missing = true; // Create database if missing.
    options.write_buffer_size = 256 * 1024 * 1024; // 256MB
    open_status = rocksdb::DB::Open(options, db_path_to, &db_to);
    if (!open_status.ok()) {
        std::cerr << "Error opening RocksDB: " << open_status.ToString() << std::endl;
        return 1;
    }
    std::cout << "Successfully opened RocksDB: " << db_path_to << std::endl;

    // Iterator for iterating through all key-value pairs
    rocksdb::Iterator* it = db_from->NewIterator(rocksdb::ReadOptions());
    rocksdb::WriteBatch batch;
    size_t batch_size_limit = 256 * 1024 * 1024; // 256MB per batch
    size_t current_batch_size = 0;

    for (it->SeekToFirst(); it->Valid(); it->Next()) {
        std::string key = it->key().ToString();
        std::string serialized_value = it->value().ToString();

        if (key == "epsilon" || key == "game_def") {
            batch.Put(key, serialized_value);
        } else {
            open_spiel::algorithms::CFRInfoStateValues info_state = open_spiel::algorithms::CFRInfoStateValues::DeserializeBinary(serialized_value);
            
            // Apply decay to cumulative regrets and policies
            // for (double& regret : info_state.cumulative_regrets) {
            //     regret *= DecayRate;
            // }
            for (double& policy : info_state.cumulative_policy) {
                policy *= DecayRate;
            }
            // for (double& value : info_state.estimated_action_values) {
            //     value *= DecayRate;
            // }
            
            std::string new_serialized_value = info_state.SerializeBinary();
            batch.Put(key, new_serialized_value);
            current_batch_size += key.size() + new_serialized_value.size();
        }
        
        // Write batch if size exceeds limit
        if (current_batch_size > batch_size_limit) {
            rocksdb::Status status = db_to->Write(rocksdb::WriteOptions(), &batch);
            if (!status.ok()) {
                std::cerr << "Error writing batch to RocksDB: " << status.ToString() << std::endl;
                delete it;
                delete db_from;
                delete db_to;
                return 1;
            }
            batch.Clear();
            current_batch_size = 0;
        }
    }
    
    // Write remaining batch
    if (current_batch_size > 0) {
        rocksdb::Status status = db_to->Write(rocksdb::WriteOptions(), &batch);
        if (!status.ok()) {
            std::cerr << "Error writing final batch to RocksDB: " << status.ToString() << std::endl;
        }
    }
    
    delete it;
    delete db_from;
    delete db_to;
    std::cout << "Successfully applied decay and saved to new database." << std::endl;
    return 0;
}