diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..b7ed9411 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,251 @@ +cmake_minimum_required(VERSION 3.14) + +project(TexasSolver LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /openmp:llvm") + +# Automatically run Qt's MOC, UIC, and RCC pre-processors +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# Find Qt5 and its required components +find_package(Qt5 COMPONENTS Core Gui Widgets LinguistTools REQUIRED) + +# Find OpenMP for multi-threading support, matching the .pro file's configuration +find_package(OpenMP REQUIRED) +# --- Source File Definitions --- + +# Core library sources (shared between GUI and CLI) +set(CORE_SOURCES + src/Deck.cpp + src/Card.cpp + src/GameTree.cpp + src/library.cpp + src/compairer/Dic5Compairer.cpp + src/experimental/TCfrSolver.cpp + src/nodes/ActionNode.cpp + src/nodes/ChanceNode.cpp + src/nodes/GameActions.cpp + src/nodes/GameTreeNode.cpp + src/nodes/ShowdownNode.cpp + src/nodes/TerminalNode.cpp + src/pybind/bindSolver.cpp + src/ranges/PrivateCards.cpp + src/ranges/PrivateCardsManager.cpp + src/ranges/RiverCombs.cpp + src/ranges/RiverRangeManager.cpp + src/runtime/PokerSolver.cpp + src/solver/BestResponse.cpp + src/solver/CfrSolver.cpp + src/solver/PCfrSolver.cpp + src/solver/Solver.cpp + src/tools/GameTreeBuildingSettings.cpp + src/tools/lookup8.cpp + src/tools/PrivateRangeConverter.cpp + src/tools/progressbar.cpp + src/tools/Rule.cpp + src/tools/StreetSetting.cpp + src/tools/utils.cpp + src/trainable/CfrPlusTrainable.cpp + src/trainable/DiscountedCfrTrainable.cpp + src/trainable/DiscountedCfrTrainableHF.cpp + src/trainable/DiscountedCfrTrainableSF.cpp + src/trainable/Trainable.cpp +) + +# GUI-specific sources +set(GUI_SOURCES + main.cpp + mainwindow.cpp + src/runtime/qsolverjob.cpp + qstextedit.cpp + strategyexplorer.cpp + qstreeview.cpp + src/ui/treeitem.cpp + src/ui/treemodel.cpp + htmltableview.cpp + src/ui/worditemdelegate.cpp + src/ui/tablestrategymodel.cpp + src/ui/strategyitemdelegate.cpp + src/ui/detailwindowsetting.cpp + src/ui/detailviewermodel.cpp + src/ui/detailitemdelegate.cpp + src/ui/roughstrategyviewermodel.cpp + src/ui/roughstrategyitemdelegate.cpp + src/ui/droptextedit.cpp + src/ui/htmltablerangeview.cpp + rangeselector.cpp + src/ui/rangeselectortablemodel.cpp + src/ui/rangeselectortabledelegate.cpp + boardselector.cpp + src/ui/boardselectortablemodel.cpp + src/ui/boardselectortabledelegate.cpp + settingeditor.cpp +) + +# Define the list of header files (useful for IDEs) +set(HEADERS + include/tools/half-1-12-0.h + include/trainable/DiscountedCfrTrainableHF.h + include/trainable/DiscountedCfrTrainableSF.h + mainwindow.h + include/Card.h + include/GameTree.h + include/Deck.h + include/json.hpp + include/library.h + include/solver/PCfrSolver.h + include/solver/Solver.h + include/solver/BestResponse.h + include/solver/CfrSolver.h + include/tools/argparse.hpp + include/tools/CommandLineTool.h + include/tools/utils.h + include/tools/GameTreeBuildingSettings.h + include/tools/Rule.h + include/tools/StreetSetting.h + include/tools/lookup8.h + include/tools/PrivateRangeConverter.h + include/tools/progressbar.h + include/runtime/PokerSolver.h + include/trainable/CfrPlusTrainable.h + include/trainable/DiscountedCfrTrainable.h + include/trainable/Trainable.h + include/compairer/Compairer.h + include/compairer/Dic5Compairer.h + include/experimental/TCfrSolver.h + include/nodes/ActionNode.h + include/nodes/ChanceNode.h + include/nodes/GameActions.h + include/nodes/GameTreeNode.h + include/nodes/ShowdownNode.h + include/nodes/TerminalNode.h + include/ranges/PrivateCards.h + include/ranges/PrivateCardsManager.h + include/ranges/RiverCombs.h + include/ranges/RiverRangeManager.h + include/tools/tinyformat.h + include/tools/qdebugstream.h + include/runtime/qsolverjob.h + qstextedit.h + strategyexplorer.h + qstreeview.h + include/ui/treeitem.h + include/ui/treemodel.h + htmltableview.h + include/ui/worditemdelegate.h + include/ui/tablestrategymodel.h + include/ui/strategyitemdelegate.h + include/ui/detailwindowsetting.h + include/ui/detailviewermodel.h + include/ui/detailitemdelegate.h + include/ui/roughstrategyviewermodel.h + include/ui/roughstrategyitemdelegate.h + include/ui/droptextedit.h + include/ui/htmltablerangeview.h + rangeselector.h + include/ui/rangeselectortablemodel.h + include/ui/rangeselectortabledelegate.h + boardselector.h + include/ui/boardselectortablemodel.h + include/ui/boardselectortabledelegate.h + settingeditor.h +) + +# Define UI forms to be processed by UIC +set(FORMS + mainwindow.ui + strategyexplorer.ui + rangeselector.ui + boardselector.ui + settingeditor.ui +) + +# Define Qt resource files to be processed by RCC +set(GUI_RESOURCES + translations.qrc +) +set(COMMON_RESOURCES + compairer.qrc +) + +# Handle translation files (.ts -> .qm) +set(TS_FILES + lang_cn.ts + lang_en.ts +) +qt5_create_translation(QM_FILES ${CMAKE_CURRENT_SOURCE_DIR} ${TS_FILES}) + +# --- GUI Application Target --- +add_executable(TexasSolverGui WIN32 MACOSX_BUNDLE + ${CORE_SOURCES} + ${GUI_SOURCES} + ${HEADERS} + ${FORMS} + ${GUI_RESOURCES} + ${COMMON_RESOURCES} +) + +# Add include directories for headers and generated files +target_include_directories(TexasSolverGui PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +# Link to required libraries +target_link_libraries(TexasSolverGui PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets) + +# Add compile definitions from the .pro file +target_compile_definitions(TexasSolverGui PRIVATE QT_DEPRECATED_WARNINGS) + +# Set optimization level for release builds +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") + +# Platform-specific settings for OpenMP and icons +if(OpenMP_FOUND) + target_link_libraries(TexasSolverGui PRIVATE OpenMP::OpenMP_CXX) +endif() + +if(WIN32) + # Set the application icon for Windows by creating a resource file + set(RC_FILE ${CMAKE_CURRENT_BINARY_DIR}/TexasSolverGui.rc) + file(WRITE ${RC_FILE} "IDI_ICON1 ICON \"imgs/texassolver_logo.ico\"\n") + target_sources(TexasSolverGui PRIVATE ${RC_FILE}) +elseif(APPLE) + # Set the application icon for macOS + set_target_properties(TexasSolverGui PROPERTIES + MACOSX_BUNDLE_ICON_FILE imgs/texassolver_logo.icns + ) +endif() + +# --- Command-Line Tool Target --- +set(CLI_SOURCES + src/console.cpp + src/tools/CommandLineTool.cpp +) + +add_executable(TexasSolverCli + ${CORE_SOURCES} + ${CLI_SOURCES} + ${COMMON_RESOURCES} +) + +# Add include directories +target_include_directories(TexasSolverCli PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) + +# Link to required libraries (note: only Qt Core is needed for the CLI) +target_link_libraries(TexasSolverCli PRIVATE Qt5::Core) + +# Link OpenMP if found +if(OpenMP_FOUND) + target_link_libraries(TexasSolverCli PRIVATE OpenMP::OpenMP_CXX) +endif() + +# Add compile definitions +target_compile_definitions(TexasSolverCli PRIVATE QT_DEPRECATED_WARNINGS) \ No newline at end of file diff --git a/TexasSolverGui.pro b/TexasSolverGui.pro index c932ba63..81fa9cdc 100644 --- a/TexasSolverGui.pro +++ b/TexasSolverGui.pro @@ -90,6 +90,7 @@ SOURCES += \ src/tools/lookup8.cpp \ src/tools/PrivateRangeConverter.cpp \ src/tools/progressbar.cpp \ + src/tools/CommandLineTool.cpp \ src/tools/Rule.cpp \ src/tools/StreetSetting.cpp \ src/tools/utils.cpp \ diff --git a/include/Card.h b/include/Card.h index 7b525852..4334c3e5 100644 --- a/include/Card.h +++ b/include/Card.h @@ -9,6 +9,8 @@ #include #include "include/tools/tinyformat.h" #include + +class Deck; // Forward declaration using namespace std; class Card { @@ -20,32 +22,32 @@ class Card { Card(); explicit Card(string card,int card_number_in_deck); Card(string card); - string getCard(); - int getCardInt(); - bool empty(); - int getNumberInDeckInt(); - static int card2int(Card card); + string getCard() const; + int getCardInt() const; + bool empty() const; + int getNumberInDeckInt() const; + static int card2int(const Card& card); static int strCard2int(string card); static string intCard2Str(int card); static uint64_t boardCards2long(vector cards); - static uint64_t boardCard2long(Card& card); - static uint64_t boardCards2long(vector& cards); - static QString boardCards2html(vector& cards); + static uint64_t boardCard2long(const Card& card); + static uint64_t boardCards2long(const vector& cards); + static QString boardCards2html(const vector& cards); static inline bool boardsHasIntercept(uint64_t board1,uint64_t board2){ return ((board1 & board2) != 0); }; static uint64_t boardInts2long(const vector& board); static uint64_t boardInt2long(int board); static vector long2board(uint64_t board_long); - static vector long2boardCards(uint64_t board_long); + static vector long2boardCards(uint64_t board_long, const Deck& deck); static string suitToString(int suit); static string rankToString(int rank); static int rankToInt(char rank); static int suitToInt(char suit); static vector getSuits(); - string toString(); - string toFormattedString(); - QString toFormattedHtml(); + string toString() const; + string toFormattedString() const; + QString toFormattedHtml() const; }; #endif //TEXASSOLVER_CARD_H diff --git a/include/Deck.h b/include/Deck.h index 1e1ccdb4..12c6b801 100644 --- a/include/Deck.h +++ b/include/Deck.h @@ -11,8 +11,8 @@ class Deck { public: Deck(); Deck(const vector& ranks, const vector& suits); - vector& getCards(); - vector& getRanks(){return this->ranks;}; + const vector& getCards() const { return this->cards; } + const vector& getRanks() const { return this->ranks; } private: vector ranks; vector suits; diff --git a/include/compairer/Dic5Compairer.h b/include/compairer/Dic5Compairer.h index 9a97f71f..2fedf054 100644 --- a/include/compairer/Dic5Compairer.h +++ b/include/compairer/Dic5Compairer.h @@ -45,6 +45,8 @@ class Dic5Compairer:public Compairer{ int getRank(vector cards); int getRank(vector cards); static CompairResult compairRanks(int rank_former,int rank_latter); + template + Compairer::CompairResult compair_template(const vector& private_former, const vector& private_latter, const vector& public_board); }; diff --git a/include/nodes/GameActions.h b/include/nodes/GameActions.h index 36d4c8ef..cca38e57 100644 --- a/include/nodes/GameActions.h +++ b/include/nodes/GameActions.h @@ -13,11 +13,11 @@ class GameActions { public: GameActions(); - GameTreeNode::PokerActions getAction(); - double getAmount(); + GameTreeNode::PokerActions getAction() const; + double getAmount() const; GameActions(GameTreeNode::PokerActions action, double amount); - string toString(); - string pokerActionToString(GameTreeNode::PokerActions pokerActions); + string toString() const; + string pokerActionToString(GameTreeNode::PokerActions pokerActions) const; private: GameTreeNode::PokerActions action; double amount{}; diff --git a/include/ranges/PrivateCards.h b/include/ranges/PrivateCards.h index b2d82bf6..c974ca5a 100644 --- a/include/ranges/PrivateCards.h +++ b/include/ranges/PrivateCards.h @@ -14,10 +14,27 @@ class PrivateCards { float relative_prob{}; PrivateCards(); PrivateCards(int card1, int card2, float weight); - uint64_t toBoardLong(); - int hashCode(); - string toString(); + uint64_t toBoardLong() const; + int hashCode() const; + string toString() const; const vector & get_hands() const; + PrivateCards exchange_color(int rank1, int rank2) const { + int new_card1 = this->card1; + int new_card2 = this->card2; + + if (this->card1 % 4 == rank1) { + new_card1 = this->card1 - rank1 + rank2; + } else if (this->card1 % 4 == rank2) { + new_card1 = this->card1 - rank2 + rank1; + } + + if (this->card2 % 4 == rank1) { + new_card2 = this->card2 - rank1 + rank2; + } else if (this->card2 % 4 == rank2) { + new_card2 = this->card2 - rank2 + rank1; + } + return PrivateCards(new_card1, new_card2, this->weight); + } private: vector card_vec; int hash_code{}; diff --git a/include/runtime/PokerSolver.h b/include/runtime/PokerSolver.h index 705919bc..c4cd0f9a 100644 --- a/include/runtime/PokerSolver.h +++ b/include/runtime/PokerSolver.h @@ -8,6 +8,7 @@ #include #include #include "include/compairer/Dic5Compairer.h" +#include "include/solver/Solver.h" #include "include/tools/PrivateRangeConverter.h" #include "include/solver/CfrSolver.h" #include "include/solver/PCfrSolver.h" @@ -52,6 +53,8 @@ class PokerSolver { vector player2Range; void dump_strategy(QString dump_file,int dump_rounds); shared_ptr get_game_tree(){return this->game_tree;}; + Solver::AnalysisMode analysis_mode = Solver::AnalysisMode::STANDARD; + string full_board; Deck* get_deck(){return &this->deck;} shared_ptr get_solver(){return this->solver;} private: diff --git a/include/runtime/qsolverjob.h b/include/runtime/qsolverjob.h index 507bb05f..f3339b2c 100644 --- a/include/runtime/qsolverjob.h +++ b/include/runtime/qsolverjob.h @@ -22,6 +22,12 @@ class QSolverJob : public QThread }; Mode mode = Mode::HOLDEM; + enum class AnalysisMode { + STANDARD, + HAND_ANALYSIS + }; + AnalysisMode analysis_mode = AnalysisMode::STANDARD; + enum MissionType{ LOADING, SOLVING, @@ -36,13 +42,14 @@ class QSolverJob : public QThread int current_round=1; int raise_limit=4; int thread_number=1; - float small_blind=0.5; - float big_blind=1; + float small_blind=0.5f; + float big_blind=1.0f; float stack=20 + 5; - float allin_threshold = 0.67; + float allin_threshold = 0.67f; string range_ip; string range_oop; string board; + string full_board; float accuracy; int max_iteration=100; int use_isomorphism=1; diff --git a/include/solver/BestResponse.h b/include/solver/BestResponse.h index a161a8a9..6a682d40 100644 --- a/include/solver/BestResponse.h +++ b/include/solver/BestResponse.h @@ -17,6 +17,8 @@ #include #include +#include "Solver.h" + using namespace std; class BestResponse { @@ -33,18 +35,11 @@ class BestResponse { int nthreads; int use_halffloats; public: - BestResponse( - vector>& private_combos, - int player_number, - PrivateCardsManager& pcm, - RiverRangeManager& rrm, - Deck& deck, - bool debug, - int color_iso_offset[][4], - GameTreeNode::GameRound split_round, - int nthreads = 1, - int use_halffloats = 0 - ); + BestResponse(vector>& private_combos, int player_number, + PrivateCardsManager& pcm, RiverRangeManager& rrm, Deck& deck, bool debug, + int color_iso_offset[][4], GameTreeNode::GameRound split_round, + int nthreads, int use_halffloats, + Solver::AnalysisMode analysis_mode, const vector& full_board_cards); float printExploitability(shared_ptr root, int iterationCount, float initial_pot, uint64_t initialBoard); float getBestReponseEv(shared_ptr node, int player,vector> reach_probs, uint64_t initialBoard,int deal); @@ -56,6 +51,9 @@ class BestResponse { vector showdownBestResponse(shared_ptr node, int player, const vector>& reach_probs,uint64_t board,int deal); int color_iso_offset[52 * 52 * 2][4]; GameTreeNode::GameRound split_round; + Solver::AnalysisMode analysis_mode; + vector full_board_cards; + uint64_t full_board_long; }; diff --git a/include/solver/PCfrSolver.h b/include/solver/PCfrSolver.h index cce4f68c..ce3521a2 100644 --- a/include/solver/PCfrSolver.h +++ b/include/solver/PCfrSolver.h @@ -90,19 +90,27 @@ class PCfrSolver:public Solver { float accuracy, bool use_isomorphism, int use_halffloats, - int num_threads + int num_threads, + Solver::AnalysisMode analysis_mode, + const string& full_board ); ~PCfrSolver(); void train() override; void stop() override; json dumps(bool with_status,int depth) override; + void dumps(std::ostream& stream, bool with_status, int depth) override; vector>> get_strategy(shared_ptr node,vector chance_cards) override; vector>> get_evs(shared_ptr node,vector chance_cards) override; + vector get_initial_board_cards() override { return this->initial_board_cards; } + vector get_full_board_cards() override { return this->full_board_cards; } + + const vector& get_full_board_cards() const { return full_board_cards; } private: vector> ranges; vector range1; vector range2; vector initial_board; + vector initial_board_cards; uint64_t initial_board_long; shared_ptr compairer; int color_iso_offset[52 * 52 * 2][4] = {0}; @@ -129,6 +137,9 @@ class PCfrSolver:public Solver { bool use_isomorphism; int use_halffloats; bool nowstop = false; + Solver::AnalysisMode analysis_mode; + vector full_board_cards; + uint64_t full_board_long; const vector& playerHands(int player); vector> getReachProbs(); @@ -143,8 +154,7 @@ class PCfrSolver:public Solver { void findGameSpecificIsomorphisms(); void purnTree(); void exchangeRange(json& strategy,int rank1,int rank2,shared_ptr one_node); - void reConvertJson(const shared_ptr& node,json& strategy,string key,int depth,int max_depth,vector prefix,int deal,vector> exchange_color_list); - + void reConvertJson(std::ostream& stream, const shared_ptr& node, const string& key, int depth, int max_depth, vector prefix, int deal, vector> exchange_color_list); }; diff --git a/include/solver/Solver.h b/include/solver/Solver.h index d7f7271b..7afc09a4 100644 --- a/include/solver/Solver.h +++ b/include/solver/Solver.h @@ -6,6 +6,7 @@ #define TEXASSOLVER_SOLVER_H +#include #include class Solver { @@ -14,14 +15,21 @@ class Solver { NONE, PUBLIC }; + enum class AnalysisMode { + STANDARD, + HAND_ANALYSIS + }; Solver(); Solver(shared_ptr tree); shared_ptr getTree(); virtual void train() = 0; virtual void stop() = 0; virtual json dumps(bool with_status,int depth) = 0; + virtual void dumps(std::ostream& stream, bool with_status, int depth) = 0; virtual vector>> get_strategy(shared_ptr node,vector cards) = 0; virtual vector>> get_evs(shared_ptr node,vector cards) = 0; + virtual vector get_initial_board_cards() { return {}; } + virtual vector get_full_board_cards() { return {}; } shared_ptr tree; }; diff --git a/include/tools/CommandLineTool.h b/include/tools/CommandLineTool.h index 3b078fa9..4a4b3939 100644 --- a/include/tools/CommandLineTool.h +++ b/include/tools/CommandLineTool.h @@ -30,13 +30,14 @@ class CommandLineTool{ int current_round=1; int raise_limit=4; int thread_number=1; - float small_blind=0.5; - float big_blind=1; + float small_blind=0.5f; + float big_blind=1.0f; float stack=20 + 5; - float allin_threshold = 0.67; + float allin_threshold = 0.67f; string range_ip; string range_oop; string board; + bool hand_analysis = false; float accuracy; int max_iteration=100; int use_isomorphism=0; diff --git a/include/trainable/DiscountedCfrTrainable.h b/include/trainable/DiscountedCfrTrainable.h index dc56aa5e..17c98acc 100644 --- a/include/trainable/DiscountedCfrTrainable.h +++ b/include/trainable/DiscountedCfrTrainable.h @@ -43,6 +43,7 @@ class DiscountedCfrTrainable:public Trainable { void copyStrategy(shared_ptr other_trainable); json dump_strategy(bool with_state) override; + void dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) override; json dump_evs() override; diff --git a/include/trainable/DiscountedCfrTrainableHF.h b/include/trainable/DiscountedCfrTrainableHF.h index 65db47c4..6fc7cf75 100644 --- a/include/trainable/DiscountedCfrTrainableHF.h +++ b/include/trainable/DiscountedCfrTrainableHF.h @@ -46,6 +46,7 @@ class DiscountedCfrTrainableHF:public Trainable { void copyStrategy(shared_ptr other_trainable); json dump_strategy(bool with_state) override; + void dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) override; json dump_evs() override; diff --git a/include/trainable/DiscountedCfrTrainableSF.h b/include/trainable/DiscountedCfrTrainableSF.h index 8b524fd2..a15c5b6e 100644 --- a/include/trainable/DiscountedCfrTrainableSF.h +++ b/include/trainable/DiscountedCfrTrainableSF.h @@ -46,6 +46,7 @@ class DiscountedCfrTrainableSF:public Trainable { void copyStrategy(shared_ptr other_trainable); json dump_strategy(bool with_state) override; + void dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) override; json dump_evs() override; diff --git a/include/trainable/Trainable.h b/include/trainable/Trainable.h index 2519e05e..f2258b47 100644 --- a/include/trainable/Trainable.h +++ b/include/trainable/Trainable.h @@ -4,8 +4,15 @@ #ifndef TEXASSOLVER_TRAINABLE_H #define TEXASSOLVER_TRAINABLE_H + #include +#include +#include #include "include/json.hpp" + +// Forward declaration to prevent circular dependency +class ActionNode; + using namespace std; using json = nlohmann::json; @@ -21,6 +28,7 @@ class Trainable { virtual void setEv(const vector& evs) = 0; virtual void copyStrategy(shared_ptr other_trainable) = 0; virtual json dump_strategy(bool with_state) = 0; + virtual void dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) = 0; virtual json dump_evs() = 0; virtual TrainableType get_type() = 0; }; diff --git a/include/ui/detailitemdelegate.h b/include/ui/detailitemdelegate.h index c673bced..3c386e92 100644 --- a/include/ui/detailitemdelegate.h +++ b/include/ui/detailitemdelegate.h @@ -20,10 +20,7 @@ class DetailItemDelegate: public WordItemDelegate{ Q_OBJECT public: - explicit DetailItemDelegate(DetailWindowSetting* detailWindowSetting,QObject *parent = 0); - -private: - DetailWindowSetting* detailWindowSetting = NULL; + explicit DetailItemDelegate(QObject *parent = nullptr); protected: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; diff --git a/include/ui/roughstrategyitemdelegate.h b/include/ui/roughstrategyitemdelegate.h index a41de197..32b98e17 100644 --- a/include/ui/roughstrategyitemdelegate.h +++ b/include/ui/roughstrategyitemdelegate.h @@ -21,10 +21,7 @@ class RoughStrategyItemDelegate: public WordItemDelegate{ Q_OBJECT public: - explicit RoughStrategyItemDelegate(DetailWindowSetting* detailWindowSetting,QObject *parent = 0); - -private: - DetailWindowSetting* detailWindowSetting = NULL; + explicit RoughStrategyItemDelegate(QObject *parent = nullptr); protected: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; diff --git a/include/ui/strategyitemdelegate.h b/include/ui/strategyitemdelegate.h index 4c40360a..7646094b 100644 --- a/include/ui/strategyitemdelegate.h +++ b/include/ui/strategyitemdelegate.h @@ -14,16 +14,13 @@ #include "include/ui/tablestrategymodel.h" #include "include/ui/detailwindowsetting.h" #include "include/library.h" +#include "worditemdelegate.h" class StrategyItemDelegate: public WordItemDelegate{ Q_OBJECT public: - explicit StrategyItemDelegate(QSolverJob * qSolverJob,DetailWindowSetting* detailWindowSetting,QObject *parent = 0); - -private: - QSolverJob * qSolverJob = NULL; - DetailWindowSetting* detailWindowSetting = NULL; + explicit StrategyItemDelegate(QObject *parent = nullptr); protected: void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; diff --git a/include/ui/tablestrategymodel.h b/include/ui/tablestrategymodel.h index ede362a2..4d32bb1c 100644 --- a/include/ui/tablestrategymodel.h +++ b/include/ui/tablestrategymodel.h @@ -2,63 +2,61 @@ #define TABLESTRATEGYMODEL_H #include -#include -#include #include "include/runtime/qsolverjob.h" -#include "include/nodes/ActionNode.h" -#include "include/nodes/ChanceNode.h" -#include "include/nodes/TerminalNode.h" -#include "include/nodes/ShowdownNode.h" #include "include/ui/treeitem.h" #include "include/nodes/GameActions.h" -#include +#include "include/ui/detailwindowsetting.h" +#include "include/Card.h" +#include +#include class TableStrategyModel : public QAbstractItemModel { Q_OBJECT public: - explicit TableStrategyModel(QSolverJob* data, QObject *parent = nullptr); - ~TableStrategyModel(); + explicit TableStrategyModel(QSolverJob *qSolverJob, DetailWindowSetting* setting, QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role) const override; - QModelIndex index(int row, int column, - const QModelIndex &parent = QModelIndex()) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole ); - QModelIndex parent(const QModelIndex &child) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; - void setGameTreeNode(TreeItem* treeItem); - void setTrunCard(Card turn_card); - void setRiverCard(Card river_card); - Card getTrunCard(){return turn_card;}; - Card getRiverCard(){return river_card;}; + + void setGameTreeNode(const weak_ptr& node); void updateStrategyData(); - const vector> get_strategy(int i,int j) const; - const vector>> get_total_strategy() const; - const vector get_ev_grid(int i,int j) const; - const vector get_strategies_evs(int i,int j) const; - vector>> current_strategy; // cardint(52) * cardint(52) * strategy_type - vector>> current_evs; // cardint(52) * cardint(52) * strategy_type - vector> p1_range; // cardint(52) * cardint(52) - vector> p2_range; // cardint(52) * cardint(52) - vector>>> ui_strategy_table; // rank * rank * (id,id) - vector>>> ui_p1_range; // rank * rank * (id,id) - vector>>> ui_p2_range; // rank * rank * (id,id) + void setTrunCard(const Card& card); + void setRiverCard(const Card& card); + Card getTrunCard() const; + Card getRiverCard() const; + weak_ptr getCurrentNode() const { return currentNode; } vector>> total_strategy; - map cardint2card; - TreeItem * treeItem = NULL;// = static_cast(index.internalPointer()); - QSolverJob* get_solver(){return this->qSolverJob;}; - int current_player; + vector> get_strategy(int i, int j) const; + vector get_strategies_evs(int i, int j) const; + QSolverJob* get_qsolverjob() const { return qSolverJob; } + const DetailWindowSetting* get_detail_window_setting() const { return detailWindowSetting; } -private: - QSolverJob* qSolverJob; - void setupModelData(); - Card turn_card; - Card river_card; + // Members needed by delegates + vector>>> ui_strategy_table; + vector>>> ui_p1_range; + vector>>> ui_p2_range; + vector>> current_strategy; + vector>> current_evs; + vector> p1_range; + vector> p2_range; + vector cardint2card; + int current_player = 0; + vector get_ev_grid(int i, int j) const; -public slots: - void clicked_event(const QModelIndex & index); +private: + QSolverJob *qSolverJob; + weak_ptr currentNode; + DetailWindowSetting* detailWindowSetting; // This pointer is owned by StrategyExplorer + Card turnCard; + Card riverCard; + void build_ui_tables(); + map> string2ij; + QStringList ranklist; }; -#endif // TABLESTRATEGYMODEL_H +#endif // TABLESTRATEGYMODEL_H \ No newline at end of file diff --git a/main.cpp b/main.cpp index 1fb0ac1f..fc7d1fc6 100644 --- a/main.cpp +++ b/main.cpp @@ -28,12 +28,8 @@ void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QS } else { - // redundant check, could be removed, or the - // upper if statement could be removed - if(MainWindow::s_textEdit != 0){ - MainWindow::s_textEdit->log_with_signal(msg); - MainWindow::s_textEdit->update(); - } + MainWindow::s_textEdit->log_with_signal(msg); + MainWindow::s_textEdit->update(); } } @@ -47,29 +43,28 @@ int main(int argc, char *argv[]) QString language_str = setting.value("language").toString(); QTranslator trans; - if(language_str == ""){ + if(language_str.isEmpty()){ QStringList languages; languages << "English" << QString::fromLocal8Bit("简体中文"); - QString lang = QInputDialog::getItem(NULL,"select language","language",languages,0,false); + QString lang = QInputDialog::getItem(nullptr,"select language","language",languages,0,false); if(lang == "English"){ - trans.load(":/lang_en.qm"); language_str = "EN"; }else if(lang == QString::fromLocal8Bit("简体中文")){ - trans.load(":/lang_cn.qm"); language_str = "CN"; } - a.installTranslator(&trans); - setting.setValue("language",language_str); - }else{ - if(language_str == "EN"){ - trans.load(":/lang_en.qm"); - }else if(language_str == "CN"){ - trans.load(":/lang_cn.qm"); + if (!language_str.isEmpty()) { + setting.setValue("language",language_str); } - a.installTranslator(&trans); } + if(language_str == "EN"){ + trans.load(":/lang_en.qm"); + }else if(language_str == "CN"){ + trans.load(":/lang_cn.qm"); + } + a.installTranslator(&trans); + int dump_round = setting.value("dump_round").toInt(); if(dump_round == 0){ setting.setValue("dump_round",2); diff --git a/mainwindow.cpp b/mainwindow.cpp index ec81aa4b..a601905b 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -19,6 +19,7 @@ MainWindow::MainWindow(QWidget *parent) : connect(this->ui->actionexport, &QAction::triggered, this, &MainWindow::on_actionexport_triggered); connect(this->ui->actionclear_all, &QAction::triggered, this, &MainWindow::on_actionclear_all_triggered); qSolverJob = new QSolverJob; + initializeParameterMap(); qSolverJob->setContext(this->getLogArea()); qSolverJob->current_mission = QSolverJob::MissionType::LOADING; qSolverJob->start(); @@ -156,115 +157,64 @@ void MainWindow::import_from_file(QString fileName){ QTextStream s1(&file); content.append(s1.readAll()); this->clear_all_params(); - for(QString one_line_content:content.split("\n")){ - if(getParams(one_line_content,"set_pot") != "INVALID"){ - this->ui->potText->setText(getParams(one_line_content,"set_pot")); - } - else if(getParams(one_line_content,"set_effective_stack") != "INVALID"){ - this->ui->effectiveStackText->setText(getParams(one_line_content,"set_effective_stack")); - } - else if(getParams(one_line_content,"set_board") != "INVALID"){ - this->ui->boardText->setText(getParams(one_line_content,"set_board")); - } - else if(getParams(one_line_content,"set_range_oop") != "INVALID"){ - this->ui->oopRangeText->setText(getParams(one_line_content,"set_range_oop")); - } - else if(getParams(one_line_content,"set_range_ip") != "INVALID"){ - this->ui->ipRangeText->setText(getParams(one_line_content,"set_range_ip")); - } - // FLOP - else if(getParams(one_line_content,"set_bet_sizes oop,flop,bet,") != "INVALID"){ - this->ui->flop_oop_bet->setText(getParams(one_line_content,"set_bet_sizes oop,flop,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,flop,raise") != "INVALID"){ - this->ui->flop_oop_raise->setText(getParams(one_line_content,"set_bet_sizes oop,flop,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,flop,allin") != "INVALID"){ - this->ui->flop_oop_allin->setChecked(true); - } - else if(getParams(one_line_content,"set_bet_sizes ip,flop,bet,") != "INVALID"){ - this->ui->flop_ip_bet->setText(getParams(one_line_content,"set_bet_sizes ip,flop,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,flop,raise") != "INVALID"){ - this->ui->flop_ip_raise->setText(getParams(one_line_content,"set_bet_sizes ip,flop,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,flop,allin") != "INVALID"){ - this->ui->flop_ip_allin->setChecked(true); - } - // TURN - else if(getParams(one_line_content,"set_bet_sizes oop,turn,bet,") != "INVALID"){ - this->ui->turn_oop_bet->setText(getParams(one_line_content,"set_bet_sizes oop,turn,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,turn,raise") != "INVALID"){ - this->ui->turn_oop_raise->setText(getParams(one_line_content,"set_bet_sizes oop,turn,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,turn,donk") != "INVALID"){ - this->ui->turn_oop_donk->setText(getParams(one_line_content,"set_bet_sizes oop,turn,donk").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,turn,allin") != "INVALID"){ - this->ui->turn_oop_allin->setChecked(true); - } - else if(getParams(one_line_content,"set_bet_sizes ip,turn,bet,") != "INVALID"){ - this->ui->turn_ip_bet->setText(getParams(one_line_content,"set_bet_sizes ip,turn,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,turn,raise") != "INVALID"){ - this->ui->turn_ip_raise->setText(getParams(one_line_content,"set_bet_sizes ip,turn,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,turn,allin") != "INVALID"){ - this->ui->turn_ip_allin->setChecked(true); - } - // RIVER - else if(getParams(one_line_content,"set_bet_sizes oop,river,bet,") != "INVALID"){ - this->ui->river_oop_bet->setText(getParams(one_line_content,"set_bet_sizes oop,river,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,river,raise") != "INVALID"){ - this->ui->river_oop_raise->setText(getParams(one_line_content,"set_bet_sizes oop,river,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,river,donk") != "INVALID"){ - this->ui->river_oop_donk->setText(getParams(one_line_content,"set_bet_sizes oop,river,donk").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes oop,river,allin") != "INVALID"){ - this->ui->river_oop_allin->setChecked(true); - } - else if(getParams(one_line_content,"set_bet_sizes ip,river,bet,") != "INVALID"){ - this->ui->river_ip_bet->setText(getParams(one_line_content,"set_bet_sizes ip,river,bet,").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,river,raise") != "INVALID"){ - this->ui->river_ip_raise->setText(getParams(one_line_content,"set_bet_sizes ip,river,raise").replace(',',' ')); - } - else if(getParams(one_line_content,"set_bet_sizes ip,river,allin") != "INVALID"){ - this->ui->river_ip_allin->setChecked(true); - } - // OTHER PARAMS - else if(getParams(one_line_content,"set_allin_threshold") != "INVALID"){ - this->ui->allinThresholdText->setText(getParams(one_line_content,"set_allin_threshold")); - } - else if(getParams(one_line_content,"set_thread_num") != "INVALID"){ - this->ui->threadsText->setText(getParams(one_line_content,"set_thread_num")); - } - else if(getParams(one_line_content,"set_accuracy") != "INVALID"){ - this->ui->exploitabilityText->setText(getParams(one_line_content,"set_accuracy")); - } - else if(getParams(one_line_content,"set_max_iteration") != "INVALID"){ - this->ui->iterationText->setText(getParams(one_line_content,"set_max_iteration")); - } - else if(getParams(one_line_content,"set_print_interval") != "INVALID"){ - this->ui->logIntervalText->setText(getParams(one_line_content,"set_print_interval")); - } - else if(getParams(one_line_content,"set_raise_limit") != "INVALID"){ - this->ui->raiseLimitText->setText(getParams(one_line_content,"set_raise_limit")); - } - else if(getParams(one_line_content,"set_use_isomorphism") != "INVALID"){ - if(getParams(one_line_content,"set_use_isomorphism") == "1"){ - this->ui->useIsoCheck->setChecked(true); - }else{ - this->ui->useIsoCheck->setChecked(false); + for(const QString& line : content.split("\n")) { + for (auto const& [key, setter] : m_parameterSetters) { + if (line.startsWith(key)) { + setter(getParams(line, key)); + break; // Move to the next line once a key is matched } } } this->update(); } +void MainWindow::initializeParameterMap() { + // Using a map to make the import logic cleaner and more extensible. + m_parameterSetters = { + {"set_pot", [this](const QString& val){ this->ui->potText->setText(val); }}, + {"set_effective_stack", [this](const QString& val){ this->ui->effectiveStackText->setText(val); }}, + {"set_board", [this](const QString& val){ this->ui->boardText->setText(val); }}, + {"set_range_oop", [this](const QString& val){ this->ui->oopRangeText->setText(val); }}, + {"set_range_ip", [this](const QString& val){ this->ui->ipRangeText->setText(val); }}, + + // FLOP + {"set_bet_sizes oop,flop,bet,", [this](const QString& val){ this->ui->flop_oop_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,flop,raise", [this](const QString& val){ this->ui->flop_oop_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,flop,allin", [this](const QString&){ this->ui->flop_oop_allin->setChecked(true); }}, + {"set_bet_sizes ip,flop,bet,", [this](const QString& val){ this->ui->flop_ip_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,flop,raise", [this](const QString& val){ this->ui->flop_ip_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,flop,allin", [this](const QString&){ this->ui->flop_ip_allin->setChecked(true); }}, + + // TURN + {"set_bet_sizes oop,turn,bet,", [this](const QString& val){ this->ui->turn_oop_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,turn,raise", [this](const QString& val){ this->ui->turn_oop_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,turn,donk", [this](const QString& val){ this->ui->turn_oop_donk->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,turn,allin", [this](const QString&){ this->ui->turn_oop_allin->setChecked(true); }}, + {"set_bet_sizes ip,turn,bet,", [this](const QString& val){ this->ui->turn_ip_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,turn,raise", [this](const QString& val){ this->ui->turn_ip_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,turn,allin", [this](const QString&){ this->ui->turn_ip_allin->setChecked(true); }}, + + // RIVER + {"set_bet_sizes oop,river,bet,", [this](const QString& val){ this->ui->river_oop_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,river,raise", [this](const QString& val){ this->ui->river_oop_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,river,donk", [this](const QString& val){ this->ui->river_oop_donk->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes oop,river,allin", [this](const QString&){ this->ui->river_oop_allin->setChecked(true); }}, + {"set_bet_sizes ip,river,bet,", [this](const QString& val){ this->ui->river_ip_bet->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,river,raise", [this](const QString& val){ this->ui->river_ip_raise->setText(QString(val).replace(',', ' ')); }}, + {"set_bet_sizes ip,river,allin", [this](const QString&){ this->ui->river_ip_allin->setChecked(true); }}, + + // OTHER PARAMS + {"set_allin_threshold", [this](const QString& val){ this->ui->allinThresholdText->setText(val); }}, + {"set_thread_num", [this](const QString& val){ this->ui->threadsText->setText(val); }}, + {"set_accuracy", [this](const QString& val){ this->ui->exploitabilityText->setText(val); }}, + {"set_max_iteration", [this](const QString& val){ this->ui->iterationText->setText(val); }}, + {"set_print_interval", [this](const QString& val){ this->ui->logIntervalText->setText(val); }}, + {"set_raise_limit", [this](const QString& val){ this->ui->raiseLimitText->setText(val); }}, + {"set_use_isomorphism", [this](const QString& val){ this->ui->useIsoCheck->setChecked(val == "1"); }}, + {"set_hand_analysis", [this](const QString& val){ this->ui->handAnalysisCheckBox->setChecked(val == "1"); }} + }; + } + void MainWindow::on_actionimport_triggered(){ QString fileName = QFileDialog::getOpenFileName( this, @@ -368,6 +318,9 @@ void MainWindow::on_actionexport_triggered(){ }else{ out << "set_use_isomorphism 0" << "\n"; } + if(this->ui->handAnalysisCheckBox->isChecked()){ + out << "set_hand_analysis 1" << "\n"; + } out << "start_solve"; out << "\n"; @@ -453,19 +406,33 @@ void MainWindow::on_buildTreeButtom_clicked() { qSolverJob->range_ip = this->ui->ipRangeText->toPlainText().toStdString(); qSolverJob->range_oop = this->ui->oopRangeText->toPlainText().toStdString(); - qSolverJob->board = this->ui->boardText->toPlainText().toStdString(); - - vector board_str_arr = string_split(qSolverJob->board,','); - if(board_str_arr.size() == 3){ - qSolverJob->current_round = 1; - }else if(board_str_arr.size() == 4){ - qSolverJob->current_round = 2; - }else if(board_str_arr.size() == 5){ - qSolverJob->current_round = 3; + + if (this->ui->handAnalysisCheckBox->isChecked()) { + qSolverJob->analysis_mode = QSolverJob::AnalysisMode::HAND_ANALYSIS; + qSolverJob->full_board = this->ui->boardText->toPlainText().toStdString(); + vector full_board_str_arr = string_split(qSolverJob->full_board, ','); + if (full_board_str_arr.size() != 5) { + this->ui->logOutput->log_with_signal(tr("Hand Analysis Mode requires a full 5-card board (e.g., As,Kd,Th,7c,2d).")); + return; + } + qSolverJob->board = full_board_str_arr[0] + "," + full_board_str_arr[1] + "," + full_board_str_arr[2]; + qSolverJob->current_round = 1; // Always start from the flop in analysis mode }else{ - this->ui->logOutput->log_with_signal(QString::fromStdString(tfm::format("Error : board %s not recognized",qSolverJob->board))); - return; + qSolverJob->analysis_mode = QSolverJob::AnalysisMode::STANDARD; + qSolverJob->board = this->ui->boardText->toPlainText().toStdString(); + vector board_str_arr = string_split(qSolverJob->board,','); + if(board_str_arr.size() == 3){ + qSolverJob->current_round = 1; + }else if(board_str_arr.size() == 4){ + qSolverJob->current_round = 2; + }else if(board_str_arr.size() == 5){ + qSolverJob->current_round = 3; + }else{ + this->ui->logOutput->log_with_signal(QString::fromStdString(tfm::format("Error : board %s not recognized",qSolverJob->board))); + return; + } } + qSolverJob->raise_limit = this->ui->raiseLimitText->text().toInt(); qSolverJob->ip_commit = this->ui->potText->text().toFloat() / 2; qSolverJob->oop_commit = this->ui->potText->text().toFloat() / 2; diff --git a/mainwindow.h b/mainwindow.h index f6ee1fd5..b96d310c 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -13,6 +13,7 @@ #include "settingeditor.h" #include "include/ui/rangeselectortablemodel.h" #include "include/ui/rangeselectortabledelegate.h" +#include namespace Ui { class MainWindow; @@ -60,6 +61,8 @@ private slots: private: void clear_all_params(); + void initializeParameterMap(); + std::map> m_parameterSetters; Ui::MainWindow *ui = NULL; QSolverJob* qSolverJob = NULL; QFileSystemModel * qFileSystemModel = NULL; diff --git a/mainwindow.ui b/mainwindow.ui index 00570485..9cc10c87 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -38,9 +38,9 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont'; font-size:13pt;">AA,KK,QQ,JJ,TT,99:0.75,88:0.75,77:0.5,66:0.25,55:0.25,AK,AQs,AQo:0.75,AJs,AJo:0.5,ATs:0.75,A6s:0.25,A5s:0.75,A4s:0.75,A3s:0.5,A2s:0.5,KQs,KQo:0.5,KJs,KTs:0.75,K5s:0.25,K4s:0.25,QJs:0.75,QTs:0.75,Q9s:0.5,JTs:0.75,J9s:0.75,J8s:0.75,T9s:0.75,T8s:0.75,T7s:0.75,98s:0.75,97s:0.75,96s:0.5,87s:0.75,86s:0.5,85s:0.5,76s:0.75,75s:0.5,65s:0.75,64s:0.5,54s:0.75,53s:0.5,43s:0.5</span></p></body></html> @@ -56,9 +56,9 @@ p, li { white-space: pre-wrap; } <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont'; font-size:13pt;">QQ:0.5,JJ:0.75,TT,99,88,77,66,55,44,33,22,AKo:0.25,AQs,AQo:0.75,AJs,AJo:0.75,ATs,ATo:0.75,A9s,A8s,A7s,A6s,A5s,A4s,A3s,A2s,KQ,KJ,KTs,KTo:0.5,K9s,K8s,K7s,K6s,K5s,K4s:0.5,K3s:0.5,K2s:0.5,QJ,QTs,Q9s,Q8s,Q7s,JTs,JTo:0.5,J9s,J8s,T9s,T8s,T7s,98s,97s,96s,87s,86s,76s,75s,65s,64s,54s,53s,43s</span></p></body></html> @@ -128,6 +128,16 @@ p, li { white-space: pre-wrap; } + + + + + + + Hand Analysis Mode + + + @@ -139,9 +149,9 @@ p, li { white-space: pre-wrap; } <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont'; font-size:13pt;">Qs,Jh,2h</span></p></body></html> @@ -715,7 +725,7 @@ p, li { white-space: pre-wrap; } - 3 + 2 @@ -729,7 +739,7 @@ p, li { white-space: pre-wrap; } - 50 + @@ -743,7 +753,7 @@ p, li { white-space: pre-wrap; } - 200 + 100 @@ -1113,7 +1123,7 @@ p, li { white-space: pre-wrap; } 0 0 1134 - 26 + 21 diff --git a/resources/jupyter/PokerTracker4-to-TexasSolver.ipynb b/resources/jupyter/PokerTracker4-to-TexasSolver.ipynb new file mode 100644 index 00000000..6bfa7588 --- /dev/null +++ b/resources/jupyter/PokerTracker4-to-TexasSolver.ipynb @@ -0,0 +1,1089 @@ +{ + "cells": [ + { + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "cell_type": "code", + "source": [ + "import sys\n", + "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", + "import re\n", + "from sqlalchemy.exc import OperationalError\n", + "\n", + "# --- Database Connection Details ---\n", + "db_user = 'postgres'\n", + "db_password = ''\n", + "db_host = 'localhost'\n", + "db_port = '5432'\n", + "db_name = 'pt4'\n", + "\n", + "# Create the database connection URL for SQLAlchemy\n", + "db_url = f\"postgresql+psycopg2://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}\"\n", + "\n", + "# --- SQL Query ---\n", + "# Using SQLAlchemy's text() construct allows for safe parameter binding.\n", + "# The :player_id is a named parameter that we will supply later.\n", + "sql_query = text(\"\"\"\n", + " SELECT s.date_played,\n", + " cl.amt_bb,\n", + " p.player_name as hero_name,\n", + " h.*\n", + " FROM cash_hand_histories AS h\n", + " JOIN cash_hand_player_statistics AS s ON h.id_hand = s.id_hand\n", + " JOIN cash_limit cl on s.id_limit = cl.id_limit\n", + " JOIN player p on s.id_player = p.id_player\n", + " WHERE s.id_player = :player_id\n", + " AND s.flg_f_saw\n", + " AND CAST(s.date_played AS DATE) = '2025-09-14'\n", + " order by id_hand desc;\n", + " \"\"\")\n", + "\n", + "# --- Parameters ---\n", + "query_params = {\"player_id\": 93} # <-- IMPORTANT: Replace 93 with your actual player ID\n", + "\n", + "# --- Main Execution Block ---\n", + "hands_df = pd.DataFrame() # Initialize an empty DataFrame\n", + "\n", + "try:\n", + " print(\"Creating database engine...\")\n", + " # Pass client_encoding directly to the DBAPI driver (psycopg2) via connect_args.\n", + " # This is a cleaner, more direct way to handle encoding issues than environment variables.\n", + " engine = create_engine(\n", + " db_url,\n", + " connect_args={'client_encoding': 'WIN1252'}\n", + " )\n", + "\n", + " print(\"\\nExecuting query and loading data into a DataFrame...\")\n", + " # Pandas can use the SQLAlchemy engine directly to manage connections.\n", + " # We pass the query parameters separately for safety.\n", + " hands_df = pd.read_sql_query(sql_query, engine, params=query_params)\n", + "\n", + " print(\"Successfully loaded data.\")\n", + " print(f\"Found {len(hands_df)} hands.\")\n", + "\n", + " # Overwrite id_hand with the value extracted from the history text\n", + " if not hands_df.empty:\n", + " print(\"\\nExtracting hand ID from history text to overwrite 'id_hand' column...\")\n", + " # This regex finds the numeric ID from the first line of the hand history.\n", + " # e.g., \"PokerStars Hand #123456789: ...\" -> \"123456789\"\n", + " hands_df['id_hand'] = hands_df['history'].str.extract(r'Hand #(\\d+)', expand=False)\n", + " hands_df['id_hand'] = pd.to_numeric(hands_df['id_hand'], errors='coerce')\n", + "\n", + " # Drop rows where the hand ID could not be extracted\n", + " hands_df.dropna(subset=['id_hand'], inplace=True)\n", + " print(\"'id_hand' column has been updated.\")\n", + "\n", + "except OperationalError as e:\n", + " print(f\"CONNECTION FAILED: Could not connect to the database.\", file=sys.stderr)\n", + " print(f\"Please check your credentials and ensure the PostgreSQL server is running.\", file=sys.stderr)\n", + " print(f\"Error details: {e}\", file=sys.stderr)\n", + "except Exception as e:\n", + " print(f\"\\nQUERY FAILED: An unexpected error occurred.\", file=sys.stderr)\n", + " print(f\"Error details: {e}\", file=sys.stderr)" + ], + "id": "aca605efb1f5d0c9", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# hands_df = hands_df[(hands_df['id_hand'] == 2546893392)]\n", + "hands_df = hands_df[hands_df['history'].str.contains(' RIVER ')]\n", + "hands_df" + ], + "id": "33cb716457487cf4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import re\n", + "\n", + "# The previous cell loaded data into 'hands_df'. This cell will process it.\n", + "\n", + "def convert_history_to_bb(row):\n", + " \"\"\"\n", + " Parses the 'history' text and converts all dollar amounts to Big Blinds.\n", + " This function is designed to be used with pandas.DataFrame.apply().\n", + " \"\"\"\n", + " history_text = row['history']\n", + " big_blind_cents = row['amt_bb']\n", + "\n", + " # Return original text if there's no history or BB amount is invalid\n", + " if pd.isna(history_text) or not big_blind_cents or big_blind_cents <= 0:\n", + " return history_text\n", + "\n", + " # Correcting a bug: amt_bb is in cents, so it must be divided by 100.\n", + " bb_dollars = big_blind_cents\n", + "\n", + " def replacer(match):\n", + " \"\"\"A nested function to replace each matched dollar amount.\"\"\"\n", + " # The captured group is the numeric value (e.g., '100.00' from '$100.00')\n", + " dollar_amount = float(match.group(1))\n", + "\n", + " bb_amount = dollar_amount / bb_dollars\n", + " # Return the amount in BB, formatted to two decimal places\n", + " return f\"{bb_amount:.2f} BB\"\n", + "\n", + " # This regex finds dollar amounts (e.g., $100.00, $5.50, $0.50).\n", + " # It looks for a '$' followed by digits and an optional decimal part.\n", + " # The parentheses create a capturing group for the numeric value.\n", + " pattern = re.compile(r'\\$(\\d+\\.?\\d*)')\n", + "\n", + " return pattern.sub(replacer, history_text)\n", + "\n", + "\n", + "# --- Main Processing Block ---\n", + "\n", + "# Check if the required columns exist. Your query `SELECT h.*` should provide 'history' and 'amt_bb'.\n", + "if 'history' in hands_df.columns and 'amt_bb' in hands_df.columns:\n", + " print(\"Parsing hand histories to convert dollar amounts to Big Blinds...\")\n", + " hands_df['history_bb'] = hands_df.apply(convert_history_to_bb, axis=1)\n", + " print(\"Conversion complete.\")\n", + "\n", + " print(\"\\nDataFrame Head with BB Conversion:\")\n", + " display(hands_df[['history', 'history_bb', 'amt_bb']].head())\n", + "else:\n", + " print(\"\\nERROR: Could not perform conversion.\", file=sys.stderr)\n", + " print(\"The DataFrame must contain 'history' and 'amt_bb' columns.\", file=sys.stderr)\n", + " print(f\"Available columns: {hands_df.columns.tolist()}\", file=sys.stderr)\n", + "hands_df" + ], + "id": "d8a47d169b41364b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import numpy as np\n", + "\n", + "def analyze_hand(row):\n", + " \"\"\"\n", + " Analyzes a hand history row to extract key information in a single pass.\n", + " This is far more efficient than parsing the same text multiple times.\n", + " \"\"\"\n", + " history_text = row['history_bb']\n", + " hero_name = row['hero_name']\n", + "\n", + " if pd.isna(history_text):\n", + " return None\n", + "\n", + " lines = history_text.split('\\n')\n", + " if len(lines) < 2:\n", + " return None # Not a valid hand history\n", + "\n", + " # 1. Find the button seat from the second line of the hand history\n", + " button_line = lines[1]\n", + " button_match = re.search(r'Seat #(\\d+) is the button', button_line)\n", + " if not button_match:\n", + " return None # Could not determine button seat\n", + " button_seat = int(button_match.group(1))\n", + "\n", + " # 2. Map all active players to their seats and get their stacks\n", + " player_stacks = {}\n", + " seat_player_map = {}\n", + " seat_pattern = re.compile(r\"Seat (\\d+): (.+?) \\((\\d+\\.?\\d*) BB\\)(?!.*is sitting out)\")\n", + " for match in seat_pattern.finditer(history_text):\n", + " seat_num = int(match.group(1))\n", + " player_name = match.group(2).strip()\n", + " stack_size = float(match.group(3))\n", + "\n", + " player_stacks[player_name] = stack_size\n", + " seat_player_map[seat_num] = player_name\n", + "\n", + " if not player_stacks or hero_name not in player_stacks:\n", + " return None\n", + "\n", + " # 3. Calculate player positions based on the button's seat number\n", + " active_seats = sorted(seat_player_map.keys())\n", + " num_players = len(active_seats)\n", + " player_positions = {}\n", + "\n", + " if num_players < 2:\n", + " return None # Not enough players for a game\n", + "\n", + " try:\n", + " button_index = active_seats.index(button_seat)\n", + " except ValueError:\n", + " return None # Button player is not in the list of active players\n", + "\n", + " # Assign positions relative to the button, handling different table sizes\n", + " if num_players == 2:\n", + " player_positions[seat_player_map[active_seats[button_index]]] = 'BTN' # In HU, BTN is SB\n", + " player_positions[seat_player_map[active_seats[(button_index + 1) % num_players]]] = 'BB'\n", + " else:\n", + " # For 3+ players\n", + " player_positions[seat_player_map[active_seats[button_index]]] = 'BTN'\n", + " player_positions[seat_player_map[active_seats[(button_index + 1) % num_players]]] = 'SB'\n", + " player_positions[seat_player_map[active_seats[(button_index + 2) % num_players]]] = 'BB'\n", + " if num_players > 3:\n", + " player_positions[seat_player_map[active_seats[(button_index - 1 + num_players) % num_players]]] = 'CO'\n", + " if num_players > 4:\n", + " player_positions[seat_player_map[active_seats[(button_index - 2 + num_players) % num_players]]] = 'MP'\n", + " if num_players > 5:\n", + " player_positions[seat_player_map[active_seats[(button_index - 3 + num_players) % num_players]]] = 'UTG'\n", + "\n", + " # This regex is designed to be flexible. It captures the player, action, and an optional\n", + " # BB amount that can appear anywhere after the action (e.g., \"raises 2.80 BB\", \"calls 4.60 BB\").\n", + " action_pattern = re.compile(r\"(.+?):? (raises|bets|calls|checks|folds)(?:.*? ([\\d\\.]+) BB)?(.*)\")\n", + "\n", + " def _parse_street_actions(street_text, known_players):\n", + " \"\"\"Helper to parse actions from a block of text for a single street.\"\"\"\n", + " actions = []\n", + " if not street_text:\n", + " return actions\n", + " for match in action_pattern.findall(street_text):\n", + " player_name = match[0].strip()\n", + " if player_name.endswith(':'):\n", + " player_name = player_name[:-1].strip()\n", + "\n", + " # Skip system messages like \"Uncalled bet...\" that don't have a known player\n", + " if player_name not in known_players:\n", + " continue\n", + "\n", + " actions.append({\n", + " 'player': player_name,\n", + " 'action': match[1],\n", + " 'amount': float(match[2]) if match[2] else 0,\n", + " 'is_all_in': 'is all-in' in match[3].strip()\n", + " })\n", + " return actions\n", + "\n", + " # Split the history into sections based on the '***' dividers\n", + " # The regex uses a lookahead to keep the dividers as part of the split result.\n", + " street_sections = re.split(r'(?=\\*\\*\\* (?:FLOP|TURN|RIVER|SHOW DOWN|SUMMARY) \\*\\*\\*)', history_text)\n", + "\n", + " preflop_text = street_sections[0]\n", + " flop_text = \"\"\n", + " turn_text = \"\"\n", + " river_text = \"\"\n", + "\n", + " # A hand might not have all streets, so we search for each one.\n", + " for section in street_sections[1:]:\n", + " if section.startswith('*** FLOP ***'):\n", + " flop_text = section\n", + " elif section.startswith('*** TURN ***'):\n", + " turn_text = section\n", + " elif section.startswith('*** RIVER ***'):\n", + " river_text = section\n", + "\n", + " preflop_actions = _parse_street_actions(preflop_text, player_stacks)\n", + " flop_actions = _parse_street_actions(flop_text, player_stacks)\n", + " turn_actions = _parse_street_actions(turn_text, player_stacks)\n", + " river_actions = _parse_street_actions(river_text, player_stacks)\n", + "\n", + " # 5. Determine players at flop from the canonical action list, ensuring consistency.\n", + " players_who_folded = {act['player'] for act in preflop_actions if act['action'] == 'folds'}\n", + " players_at_start = set(player_stacks.keys())\n", + " players_who_saw_flop = players_at_start - players_who_folded\n", + "\n", + " # Create a dictionary of positions for only the players who saw the flop.\n", + " active_player_positions = {p: player_positions[p] for p in players_who_saw_flop if p in player_positions}\n", + "\n", + " # 6. Parse board cards\n", + " board_cards = {}\n", + " flop_match = re.search(r'\\*\\*\\* FLOP \\*\\*\\* \\[(.+?)\\]', history_text)\n", + " if flop_match:\n", + " board_cards['flop'] = flop_match.group(1).split(' ')\n", + "\n", + " turn_match = re.search(r'\\*\\*\\* TURN \\*\\*\\* .*?\\[([A-Za-z0-9]{2})\\]', history_text)\n", + " if turn_match:\n", + " board_cards['turn'] = turn_match.group(1)\n", + "\n", + " river_match = re.search(r'\\*\\*\\* RIVER \\*\\*\\* .*?\\[([A-Za-z0-9]{2})\\]', history_text)\n", + " if river_match:\n", + " board_cards['river'] = river_match.group(1)\n", + "\n", + " # 7. Extract Hero's cards\n", + " hero_cards_match = re.search(rf\"Dealt to {re.escape(hero_name)} \\[([A-Za-z0-9\\s]+)\\]\", history_text)\n", + " hero_cards = None\n", + " if hero_cards_match:\n", + " # e.g., \"Ac 7c\" -> \"Ac7c\"\n", + " hero_cards = hero_cards_match.group(1).replace(' ', '')\n", + "\n", + " return {\n", + " \"players_at_flop\": len(players_who_saw_flop),\n", + " \"active_players\": list(players_who_saw_flop),\n", + " \"active_player_positions\": active_player_positions,\n", + " \"player_stacks\": player_stacks,\n", + " \"player_positions\": player_positions,\n", + " \"preflop_actions\": preflop_actions,\n", + " \"flop_actions\": flop_actions,\n", + " \"turn_actions\": turn_actions,\n", + " \"river_actions\": river_actions,\n", + " \"board_cards\": board_cards,\n", + " \"hero_cards\": hero_cards,\n", + " }\n", + "\n", + "\n", + "print(\"Analyzing all hand histories...\")\n", + "hands_df['analysis'] = hands_df.apply(analyze_hand, axis=1)\n", + "hands_df.dropna(subset=['analysis'], inplace=True) # Drop hands that couldn't be parsed\n", + "print(\"Analysis complete.\")\n", + "\n", + "# --- Filter for Heads-Up (2-Player) Flops ---\n", + "initial_hand_count = len(hands_df)\n", + "hands_df = hands_df[hands_df['analysis'].apply(lambda x: x['players_at_flop'] == 2)].copy()\n", + "print(f\"\\nFiltered for heads-up pots. Kept {len(hands_df)} of {initial_hand_count} hands.\")\n", + "hands_df" + ], + "id": "ed18b6d4f1289ac6", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "import re\n", + "\n", + "RANGE_FOLDER = '6max_range'\n", + "CACHED_RANGES = {} # Cache ranges to avoid reading the same file multiple times\n", + "\n", + "\n", + "def find_closest_size_dir(parent_path, target_size_bb):\n", + " \"\"\"Finds the subdirectory in parent_path that is numerically closest to the target bet size.\"\"\"\n", + " if not os.path.isdir(parent_path):\n", + " return None\n", + "\n", + " size_dirs = {}\n", + " for subdir in os.listdir(parent_path):\n", + " # Check if the entry is a directory and matches the bet size format (e.g., \"2.5bb\")\n", + " if os.path.isdir(os.path.join(parent_path, subdir)):\n", + " match = re.match(r'(\\d+\\.?\\d*)bb', subdir)\n", + " if match:\n", + " size_dirs[float(match.group(1))] = subdir\n", + "\n", + " if not size_dirs:\n", + " return None\n", + "\n", + " # Find the key (size) in the dictionary that is closest to the target size\n", + " closest_size = min(size_dirs.keys(), key=lambda k: abs(k - target_size_bb))\n", + " return size_dirs[closest_size]\n", + "\n", + "\n", + "def load_range_from_file(filepath):\n", + " \"\"\"Loads a hand range from a file path and caches the result.\"\"\"\n", + " if not filepath:\n", + " return None\n", + " if filepath in CACHED_RANGES:\n", + " return CACHED_RANGES[filepath]\n", + "\n", + " try:\n", + " with open(filepath, 'r') as f:\n", + " range_text = f.read().strip()\n", + " CACHED_RANGES[filepath] = range_text\n", + " return range_text\n", + " except FileNotFoundError:\n", + " # Cache the failure so we don't try again\n", + " CACHED_RANGES[filepath] = None\n", + " return None\n", + "\n", + "\n", + "def assign_ranges(row):\n", + " \"\"\"Dynamically builds a path based on pre-flop actions to find and load the correct solver ranges.\"\"\"\n", + " analysis = row['analysis']\n", + " amt_bb = row['amt_bb']\n", + "\n", + " if not analysis or analysis['players_at_flop'] != 2:\n", + " return None, None\n", + "\n", + " players = analysis['active_players']\n", + " positions = analysis['player_positions']\n", + "\n", + " # Determine who is In Position (IP) and Out of Position (OOP)\n", + " # The player who acts last post-flop is in position.\n", + " p1, p2 = players[0], players[1]\n", + " p1_pos, p2_pos = positions.get(p1), positions.get(p2)\n", + "\n", + " post_flop_order = ['SB', 'BB', 'UTG', 'MP', 'CO', 'BTN']\n", + " try:\n", + " if post_flop_order.index(p1_pos) > post_flop_order.index(p2_pos):\n", + " ip_player, oop_player = p1, p2\n", + " else:\n", + " ip_player, oop_player = p2, p1\n", + " except (ValueError, TypeError):\n", + " # This can happen if a position is not in our standard list\n", + " return None, None\n", + "\n", + " ip_pos = positions.get(ip_player)\n", + " oop_pos = positions.get(oop_player)\n", + "\n", + " # --- Build path based on action sequence ---\n", + " current_path = RANGE_FOLDER\n", + "\n", + " # First, identify players who have voluntarily put money in the pot (VPIP).\n", + " # A fold is only a significant path segment if the player was already \"in\".\n", + " vpip_players = {\n", + " act['player'] for act in analysis['preflop_actions']\n", + " if act['action'] in ['raises', 'bets', 'calls']\n", + " }\n", + "\n", + " for act in analysis['preflop_actions']:\n", + " player_pos = positions.get(act['player'])\n", + " if not player_pos:\n", + " continue\n", + "\n", + " # LOGIC CHANGE: Skip passive folds that don't create a new branch in the solver tree.\n", + " if act['action'] == 'folds' and act['player'] not in vpip_players:\n", + " continue\n", + "\n", + " # Append player position\n", + " next_path_segment = os.path.join(current_path, player_pos)\n", + " if not os.path.isdir(next_path_segment):\n", + " return None, None\n", + " current_path = next_path_segment\n", + "\n", + " # Append action description\n", + " action = act['action']\n", + " action_dir = None\n", + " if action in ['raises', 'bets']:\n", + " if act['is_all_in']:\n", + " action_dir = 'AllIn'\n", + " else:\n", + " bet_size_bb = act['amount'] / amt_bb\n", + " action_dir = find_closest_size_dir(current_path, bet_size_bb)\n", + " elif action in ['calls', 'folds', 'checks']:\n", + " # Correctly map plural actions ('calls', 'folds') to singular directory names ('Call', 'Fold')\n", + " action_dir = action.rstrip('s').capitalize()\n", + "\n", + " if not action_dir:\n", + " return None, None\n", + "\n", + " next_path_segment = os.path.join(current_path, action_dir)\n", + " if not os.path.isdir(next_path_segment):\n", + " return None, None\n", + " current_path = next_path_segment\n", + "\n", + " # --- Load ranges from the final path ---\n", + " ip_range_file = os.path.join(current_path, f\"{ip_pos}_range.txt\")\n", + " oop_range_file = os.path.join(current_path, f\"{oop_pos}_range.txt\")\n", + "\n", + " ip_range = load_range_from_file(ip_range_file)\n", + " oop_range = load_range_from_file(oop_range_file)\n", + "\n", + " return ip_range, oop_range\n", + "\n", + "\n", + "print(\"\\nAssigning hand ranges based on pre-flop action...\")\n", + "hands_df[['ip_range', 'oop_range']] = hands_df.apply(assign_ranges, axis=1).apply(pd.Series)\n", + "print(\"Range assignment complete.\")\n", + "\n", + "print(\"\\nDataFrame with Hand Ranges:\")\n", + "display(hands_df[['history', 'ip_range', 'oop_range']].head())" + ], + "id": "b0b91f8138ffe7e6", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "TODO: find SB calling ranges - For now removing 0 columns", + "id": "40607c31b2a6d840" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# --- Filter out hands where ranges could not be found ---\n", + "print(f\"\\nNumber of hands before filtering out null ranges: {len(hands_df)}\")\n", + "\n", + "# Keep rows where both ip_range and oop_range were successfully assigned.\n", + "hands_df = hands_df[hands_df['ip_range'].notna() & hands_df['oop_range'].notna()].copy()\n", + "\n", + "print(f\"Number of hands after filtering: {len(hands_df)}\")\n", + "\n", + "print(\"\\nDataFrame Head after filtering for valid ranges:\")\n", + "display(hands_df[['history', 'ip_range', 'oop_range']].head())" + ], + "id": "6073a775ca1e9fa3", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "import math\n", + "\n", + "# --- Create Directories ---\n", + "input_dir = 'C:\\\\Users\\\\Braindead\\\\PyCharmMiscProject\\\\input'\n", + "output_dir = 'C:\\\\Users\\\\Braindead\\\\PyCharmMiscProject\\\\output'\n", + "os.makedirs(input_dir, exist_ok=True)\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "\n", + "def generate_solver_file(row):\n", + " \"\"\"\n", + " Generates a Texas Solver input file for a single hand history,\n", + " using pre-computed data from the 'analysis' column.\n", + " \"\"\"\n", + " try:\n", + " # --- 1. Extract Data from the Row ---\n", + " analysis = row['analysis']\n", + " history_bb = row['history_bb'] # Use BB history for parsing blinds/board\n", + " ip_range = row['ip_range']\n", + " oop_range = row['oop_range']\n", + " id_hand = row['id_hand']\n", + "\n", + " # --- 2. Determine IP and OOP Players (no change from original) ---\n", + " players = analysis['active_players']\n", + " positions = analysis['player_positions']\n", + " p1, p2 = players[0], players[1]\n", + " p1_pos, p2_pos = positions.get(p1), positions.get(p2)\n", + "\n", + " post_flop_order = ['SB', 'BB', 'UTG', 'MP', 'CO', 'BTN']\n", + " if post_flop_order.index(p1_pos) > post_flop_order.index(p2_pos):\n", + " ip_player, oop_player = p1, p2\n", + " else:\n", + " ip_player, oop_player = p2, p1\n", + "\n", + " # --- 3. Calculate Pot and Effective Stack at the START of the Flop ---\n", + " # Determine pre-flop investment for each player to calculate the pre-rake pot\n", + " player_investments = {}\n", + "\n", + " # Parse blinds from the BB history text (as this is not in 'analysis' yet)\n", + " blind_pattern = re.compile(r\"(.+?):? posts the (small|big) blind ([\\d\\.]+) BB\")\n", + " for match in blind_pattern.finditer(history_bb):\n", + " player = match.group(1).strip()\n", + " amount = float(match.group(3))\n", + " player_investments[player] = amount\n", + "\n", + " # Process pre-flop actions from 'analysis' to find total investment per player\n", + " current_bet_level = max(player_investments.values()) if player_investments else 0\n", + " for act in analysis['preflop_actions']:\n", + " player = act['player']\n", + " action = act['action']\n", + " if action == 'raises':\n", + " amount = act['amount']\n", + " player_investments[player] = amount\n", + " current_bet_level = amount\n", + " elif action == 'calls':\n", + " player_investments[player] = current_bet_level\n", + " elif action == 'bets': # Handles open-limps\n", + " amount = act['amount']\n", + " player_investments[player] = amount\n", + " current_bet_level = amount\n", + "\n", + " # The total pre-rake pot is the sum of all investments\n", + " flop_pot_bb = sum(player_investments.values())\n", + "\n", + " # Calculate effective stack at the start of the flop\n", + " p1_start_stack_bb = analysis['player_stacks'][p1]\n", + " p2_start_stack_bb = analysis['player_stacks'][p2]\n", + "\n", + " p1_investment = player_investments.get(p1, 0)\n", + " p2_investment = player_investments.get(p2, 0)\n", + "\n", + " p1_stack_at_flop = p1_start_stack_bb - p1_investment\n", + " p2_stack_at_flop = p2_start_stack_bb - p2_investment\n", + "\n", + " effective_stack_bb = min(p1_stack_at_flop, p2_stack_at_flop)\n", + "\n", + " # --- 4. Parse Board Cards (but only provide the flop to the solver) ---\n", + " flop_match = re.search(r\"\\*\\*\\* FLOP \\*\\*\\* \\[(.+?)\\]\", history_bb)\n", + " if not flop_match:\n", + " return False\n", + "\n", + " board = flop_match.group(1).replace(' ', ',')\n", + "\n", + " # --- 5. Define Bet Sizes for the Game Tree ---\n", + " standard_bet_sizes = {33, 66}\n", + " custom_bet_sizes = set()\n", + "\n", + " pot_on_street = flop_pot_bb\n", + " street_actions_list = [\n", + " analysis.get('flop_actions', []),\n", + " analysis.get('turn_actions', []),\n", + " analysis.get('river_actions', [])\n", + " ]\n", + "\n", + " for street_actions in street_actions_list:\n", + " if not street_actions:\n", + " continue\n", + "\n", + " pot_for_this_street_actions = pot_on_street\n", + " for act in street_actions:\n", + " if act['action'] in ['bets', 'raises']:\n", + " bet_amount_bb = act['amount']\n", + " if pot_for_this_street_actions > 0:\n", + " bet_pct = (bet_amount_bb / pot_for_this_street_actions) * 100\n", + "\n", + " is_significantly_different = True\n", + " for std_size in standard_bet_sizes:\n", + " if abs(bet_pct - std_size) / std_size <= 0.25:\n", + " is_significantly_different = False\n", + " break\n", + "\n", + " if is_significantly_different:\n", + " rounded_pct = int(round(bet_pct, -1))\n", + " if rounded_pct > 0:\n", + " custom_bet_sizes.add(rounded_pct)\n", + "\n", + " if act['action'] in ['bets', 'calls', 'raises']:\n", + " pot_for_this_street_actions += act['amount']\n", + "\n", + " money_in_street = sum(\n", + " act['amount'] for act in street_actions if act['action'] in ['bets', 'calls', 'raises'])\n", + " pot_on_street += money_in_street\n", + "\n", + " final_bet_sizes = sorted(list(standard_bet_sizes.union(custom_bet_sizes)))\n", + "\n", + " # --- 6. Assemble the Solver Input File ---\n", + " solver_input_template = f\"\"\"set_pot {flop_pot_bb:.2f}\n", + "set_effective_stack {effective_stack_bb:.2f}\n", + "set_board {board}\n", + "set_range_ip {ip_range}\n", + "set_range_oop {oop_range}\n", + "\"\"\"\n", + "\n", + " # Add the bet size lines for all streets and actions\n", + " bet_sizes_str = \",\".join(map(str, final_bet_sizes)) if final_bet_sizes else \"33,66\"\n", + " streets_to_set = ['flop', 'turn', 'river']\n", + " actions_to_set = ['bet', 'raise']\n", + "\n", + " for street in streets_to_set:\n", + " for action in actions_to_set:\n", + " solver_input_template += f\"set_bet_sizes oop,{street},{action},{bet_sizes_str}\\n\"\n", + " solver_input_template += f\"set_bet_sizes ip,{street},{action},{bet_sizes_str}\\n\"\n", + "\n", + " # Add the special 'donk' bet for OOP on the river\n", + " solver_input_template += f\"set_bet_sizes oop,river,donk,{bet_sizes_str}\\n\"\n", + "\n", + " # Add the static solver commands\n", + " solver_input_template += f\"\"\"set_allin_threshold 1.0\n", + "set_raise_limit 3\n", + "build_tree\n", + "set_thread_num 8\n", + "set_accuracy 0.5\n", + "set_max_iteration 200\n", + "set_print_interval 10\n", + "set_use_isomorphism 1\n", + "set_hand_analysis 1\n", + "start_solve\n", + "set_dump_rounds 3\n", + "dump_result {os.path.join(output_dir, f'{id_hand}.json')}\n", + "\"\"\"\n", + "\n", + " # --- 7. Write the File ---\n", + " input_filepath = os.path.join(input_dir, f\"{id_hand}.txt\")\n", + " with open(input_filepath, 'w') as f:\n", + " f.write(solver_input_template)\n", + "\n", + " return True\n", + "\n", + " except Exception as e:\n", + " print(f\"Failed to process hand {row.get('id_hand', 'N/A')}: {e}\", file=sys.stderr)\n", + " return False\n", + "\n", + "\n", + "# --- Main Execution Block ---\n", + "print(f\"\\nGenerating solver input files for {len(hands_df)} hands...\")\n", + "\n", + "if not hands_df.empty:\n", + " success_count = hands_df.apply(generate_solver_file, axis=1).sum()\n", + " print(\n", + " f\"Successfully generated {success_count} of {len(hands_df)} solver input files in the '{input_dir}\\\\' directory.\")\n", + "else:\n", + " print(\"No hands to process.\")\n", + "\n", + "hands_df" + ], + "id": "f3b88bbb8e21badb", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import subprocess\n", + "import os\n", + "from tqdm import tqdm\n", + "\n", + "# --- Configuration ---\n", + "# Paths to the solver executable and its resources directory.\n", + "# Using raw strings (r'...') is a good practice for Windows paths.\n", + "SOLVER_EXE_PATH = r'C:\\Users\\Braindead\\git\\TexasSolver\\cmake-build-debug\\TexasSolverCli.exe'\n", + "RESOURCE_DIR_PATH = r'C:\\Users\\Braindead\\git\\TexasSolver\\resources'\n", + "\n", + "# The input directory where .txt files were generated in the previous cell.\n", + "# This should match the 'input_dir' variable from the previous cell.\n", + "INPUT_DIR = r'C:\\Users\\Braindead\\PyCharmMiscProject\\input'\n", + "\n", + "# Number of parallel solver processes to run, as requested.\n", + "MAX_WORKERS = 4\n", + "\n", + "\n", + "def run_solver_command(command: list) -> (str, bool, str):\n", + " \"\"\"\n", + " Executes a TexasSolver command-line tool command.\n", + "\n", + " Args:\n", + " command: A list of strings representing the command to execute.\n", + "\n", + " Returns:\n", + " A tuple containing: (hand_id_str, success_flag, message).\n", + " \"\"\"\n", + " # Extract hand_id from command for logging purposes\n", + " try:\n", + " input_file_path = command[command.index('--input_file') + 1]\n", + " hand_id = os.path.basename(input_file_path).split('.')[0]\n", + " except (ValueError, IndexError):\n", + " hand_id = \"Unknown\"\n", + "\n", + " # Print the command that will be executed.\n", + " print(f\"Executing for hand {hand_id}: {' '.join(command)}\", flush=True)\n", + "\n", + " try:\n", + " # Execute the command. We capture the output to check for errors.\n", + " # A timeout is added to prevent processes from hanging indefinitely.\n", + " subprocess.run(\n", + " command,\n", + " check=True, # Raise an exception for non-zero exit codes\n", + " capture_output=True,\n", + " text=True, # Decode stdout/stderr as text\n", + " timeout=600 # 10-minute timeout per hand\n", + " )\n", + " return hand_id, True, \"Solver completed successfully.\"\n", + " except subprocess.CalledProcessError as e:\n", + " # This occurs if the solver returns a non-zero exit code (i.e., an error).\n", + " command_str = ' '.join(e.cmd)\n", + " error_message = (\n", + " f\"Solver failed with exit code {e.returncode}.\\n\"\n", + " f\"COMMAND: {command_str}\\n\"\n", + " f\"STDOUT:\\n{e.stdout}\\n\"\n", + " f\"STDERR:\\n{e.stderr}\"\n", + " )\n", + " return hand_id, False, error_message\n", + " except subprocess.TimeoutExpired as e:\n", + " command_str = ' '.join(e.cmd)\n", + " error_message = (\n", + " f\"Solver process timed out after {e.timeout} seconds.\\n\"\n", + " f\"COMMAND: {command_str}\\n\"\n", + " f\"STDOUT:\\n{e.stdout or ''}\\n\"\n", + " f\"STDERR:\\n{e.stderr or ''}\"\n", + " )\n", + " return hand_id, False, error_message\n", + " except Exception as e:\n", + " return hand_id, False, f\"An unexpected error occurred: {e}\"\n", + "\n", + "\n", + "# --- Main Execution Block ---\n", + "if 'hands_df' not in globals() or hands_df.empty:\n", + " print(\"ERROR: `hands_df` is not defined or is empty. Please run the previous cells first.\")\n", + "else:\n", + " print(f\"Preparing to run solver for {len(hands_df)} hands...\")\n", + "\n", + " # 1. Create a full list of commands to execute\n", + " commands_to_run = []\n", + " for _, row in hands_df.iterrows():\n", + " hand_id = row['id_hand']\n", + " input_file = os.path.join(INPUT_DIR, f\"{hand_id}.txt\")\n", + " if os.path.exists(input_file):\n", + " command = [\n", + " SOLVER_EXE_PATH,\n", + " '--input_file', input_file,\n", + " '--resource_dir', RESOURCE_DIR_PATH\n", + " ]\n", + " commands_to_run.append(command)\n", + " else:\n", + " print(f\"WARNING: Input file for hand {hand_id} not found. Skipping.\")\n", + "\n", + " print(f\"Starting solver for {len(commands_to_run)} hands sequentially...\")\n", + "\n", + " # 2. Run commands sequentially for debugging and clearer output\n", + " success_count = 0\n", + " failure_count = 0\n", + " for command in tqdm(commands_to_run, desc=\"Processing hands sequentially\"):\n", + " hand_id, success, message = run_solver_command(command)\n", + " if success:\n", + " success_count += 1\n", + " print(f\"Hand {hand_id}: SUCCESS.\")\n", + " else:\n", + " failure_count += 1\n", + " print(f\"Hand {hand_id}: FAILED. Reason: {message}\")\n", + "\n", + " print(\"\\n--- Solver Execution Summary ---\")\n", + " print(f\"Successfully processed: {success_count}\")\n", + " print(f\"Failed to process: {failure_count}\")\n", + " print(\"--------------------------------\")" + ], + "id": "c08ce96e6a927c3a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import json\n", + "import re\n", + "from typing import List, Dict, Any\n", + "\n", + "\n", + "def load_json_data(filepath: str) -> Dict[str, Any]:\n", + " \"\"\"Loads the solver's JSON output file.\"\"\"\n", + " try:\n", + " with open(filepath, 'r') as f:\n", + " return json.load(f)\n", + " except FileNotFoundError:\n", + " print(f\"Error: The file '{filepath}' was not found.\")\n", + " return None\n", + " except json.JSONDecodeError:\n", + " print(f\"Error: The file '{filepath}' is not a valid JSON file.\")\n", + " return None\n", + "\n", + "\n", + "def find_closest_action(available_actions: List[str], target_action: str) -> str:\n", + " \"\"\"\n", + " Finds the closest matching action from a list of available actions.\n", + " For 'CALL' or 'CHECK', it requires an exact match.\n", + " For 'BET' or 'RAISE', it finds the numerically closest size.\n", + " \"\"\"\n", + " if target_action in ['CALL', 'CHECK', 'FOLD']:\n", + " return target_action if target_action in available_actions else None\n", + "\n", + " target_match = re.match(r'(BET|RAISE) (\\d+),000000', target_action)\n", + " if not target_match:\n", + " return None\n", + "\n", + " target_type = target_match.group(1)\n", + " target_amount = int(target_match.group(2))\n", + "\n", + " sized_actions = {}\n", + " for action in available_actions:\n", + " action_match = re.match(rf'{target_type} (\\d+),000000', action)\n", + " if action_match:\n", + " amount = int(action_match.group(1))\n", + " sized_actions[action] = amount\n", + "\n", + " if not sized_actions:\n", + " return None\n", + "\n", + " closest_action = min(\n", + " sized_actions.keys(),\n", + " key=lambda k: abs(sized_actions[k] - target_amount)\n", + " )\n", + " return closest_action\n", + "\n", + "\n", + "def get_hand_strategy(node: Dict[str, Any], hand: str) -> Dict[str, float]:\n", + " \"\"\"\n", + " Analyzes and returns the strategy for a specific hand at a given node.\n", + " \"\"\"\n", + " if not node or node.get('node_type') != 'action_node':\n", + " return None\n", + "\n", + " strategy_data = node.get('strategy')\n", + " if not strategy_data:\n", + " return None\n", + "\n", + " actions = strategy_data.get('actions')\n", + " hand_strategies = strategy_data.get('strategy')\n", + "\n", + " if not actions or not hand_strategies:\n", + " return None\n", + "\n", + " hand_reversed = hand[2:] + hand[:2]\n", + " strategy_values = None\n", + " if hand in hand_strategies:\n", + " strategy_values = hand_strategies[hand]\n", + " elif hand_reversed in hand_strategies:\n", + " strategy_values = hand_strategies[hand_reversed]\n", + "\n", + " if strategy_values is None:\n", + " return None\n", + "\n", + " strategy_dict = {}\n", + " for i, action in enumerate(actions):\n", + " clean_action = action.replace(',', '').replace('000000', '')\n", + " probability = strategy_values[i]\n", + " strategy_dict[clean_action] = probability\n", + "\n", + " return strategy_dict\n", + "\n", + "\n", + "def action_to_path_string(act: Dict[str, Any]) -> str:\n", + " \"\"\"Converts an action dictionary from analysis into a solver-compatible path string.\"\"\"\n", + " action = act['action'].upper()\n", + " if action in ['BETS', 'RAISES']:\n", + " amount = int(act['amount'])\n", + " return f\"{action.rstrip('S')} {amount},000000\"\n", + " elif action == 'CALLS':\n", + " return 'CALL'\n", + " elif action == 'CHECKS':\n", + " return 'CHECK'\n", + " elif action == 'FOLDS':\n", + " return 'FOLD'\n", + " return None\n", + "\n", + "\n", + "def analyze_hero_play_vs_solver(row: pd.Series) -> List[Dict[str, Any]]:\n", + " \"\"\"\n", + " Analyzes a single hand (row) against its corresponding GTO solver output.\n", + " This function traverses the game tree based on the hand history and records\n", + " the GTO strategy at each of the hero's decision points.\n", + " \"\"\"\n", + " hand_id = row['id_hand']\n", + " analysis_data = row['analysis']\n", + " hero_name = row['hero_name']\n", + "\n", + " solver_filepath = f'C:/Users/Braindead/PyCharmMiscProject/output/{hand_id}.json'\n", + " solver_tree = load_json_data(solver_filepath)\n", + " if not solver_tree:\n", + " print(f\"Warning: Could not load solver file for hand {hand_id}. Skipping analysis for this hand.\")\n", + " return None\n", + "\n", + " hero_cards = analysis_data.get('hero_cards')\n", + " if not hero_cards:\n", + " print(f\"Warning: Could not determine hero's cards for hand {hand_id}. Skipping.\")\n", + " return None\n", + "\n", + " # --- Build a single path of actions and cards for traversal ---\n", + " # --- Build a single path of actions and cards for traversal ---\n", + " path_components = []\n", + " flop_actions = analysis_data.get('flop_actions', [])\n", + " for act in flop_actions:\n", + " path_components.append({'type': 'action', 'data': act, 'street': 'Flop'})\n", + " \n", + " turn_card = analysis_data.get('board_cards', {}).get('turn')\n", + " if turn_card:\n", + " path_components.append({'type': 'card', 'data': turn_card, 'street': 'Turn'})\n", + " turn_actions = analysis_data.get('turn_actions', [])\n", + " for act in turn_actions:\n", + " path_components.append({'type': 'action', 'data': act, 'street': 'Turn'})\n", + " \n", + " river_card = analysis_data.get('board_cards', {}).get('river')\n", + " if river_card:\n", + " path_components.append({'type': 'card', 'data': river_card, 'street': 'River'})\n", + " river_actions = analysis_data.get('river_actions', [])\n", + " for act in river_actions:\n", + " path_components.append({'type': 'action', 'data': act, 'street': 'River'})\n", + " \n", + " # --- Traverse the tree once, collecting hero's actions ---\n", + " current_node = solver_tree\n", + " hero_actions_summary = []\n", + " \n", + " for component in path_components:\n", + " if not current_node:\n", + " # This can happen if the path is invalid or the tree is corrupt.\n", + " # We stop further analysis for this hand but keep what we have.\n", + " break\n", + " \n", + " component_type = component['type']\n", + " \n", + " if component_type == 'action':\n", + " act = component['data']\n", + " \n", + " # If it's the hero's turn, analyze the decision before moving to the next node\n", + " if act['player'] == hero_name:\n", + " gto_strategy = get_hand_strategy(current_node, hero_cards)\n", + " action_str = action_to_path_string(act)\n", + " if action_str:\n", + " clean_action = action_str.replace(',', '').replace('000000', '')\n", + " hero_actions_summary.append({\n", + " 'street': component['street'],\n", + " 'hand': hero_cards,\n", + " 'player_action': clean_action,\n", + " 'gto_strategy': gto_strategy\n", + " })\n", + " \n", + " # Now, traverse to the next node based on the action taken\n", + " action_path_str = action_to_path_string(act)\n", + " if not action_path_str:\n", + " continue\n", + " \n", + " node_type = current_node.get('node_type')\n", + " if node_type == 'action_node':\n", + " children = current_node.get('childrens', {})\n", + " if action_path_str in children:\n", + " current_node = children[action_path_str]\n", + " else:\n", + " closest_action = find_closest_action(list(children.keys()), action_path_str)\n", + " if closest_action:\n", + " current_node = children[closest_action]\n", + " else:\n", + " current_node = None\n", + " else:\n", + " current_node = None # Invalid state\n", + " \n", + " elif component_type == 'card':\n", + " card = component['data']\n", + " node_type = current_node.get('node_type')\n", + " if node_type == 'chance_node':\n", + " dealcards = current_node.get('dealcards', {})\n", + " current_node = dealcards.get(card)\n", + " else:\n", + " current_node = None # Invalid state\n", + " \n", + " return hero_actions_summary if hero_actions_summary else None\n", + "\n", + "\n", + "def main():\n", + " \"\"\"\n", + " Main function to run the GTO analysis for all hands in the DataFrame\n", + " and store the results in a new column.\n", + " \"\"\"\n", + " if 'hands_df' not in globals() or hands_df.empty:\n", + " print(\"Error: `hands_df` is not defined or is empty. Please run the previous cells.\")\n", + " return\n", + "\n", + " print(f\"Starting GTO analysis for {len(hands_df)} hand(s)...\")\n", + "\n", + " # Apply the analysis function to each row and store the result in a new column\n", + " hands_df['gto_analysis'] = hands_df.apply(analyze_hero_play_vs_solver, axis=1)\n", + "\n", + " print(\"GTO analysis complete.\")\n", + "\n", + " # Display a summary of the results\n", + " successful_analyses = hands_df['gto_analysis'].notna().sum()\n", + " print(f\"Successfully analyzed {successful_analyses} of {len(hands_df)} hands.\")\n", + "\n", + " print(\"\\nDataFrame with GTO analysis results:\")\n", + " # Show relevant columns, including the new one\n", + " display(hands_df[['id_hand', 'hero_name', 'hero_cards', 'gto_analysis']].head())\n", + "\n", + "if __name__ == '__main__':\n", + " main()\n" + ], + "id": "f3dcb75d3bf9a36b", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/Card.cpp b/src/Card.cpp index ca91ac19..74916079 100644 --- a/src/Card.cpp +++ b/src/Card.cpp @@ -2,7 +2,9 @@ // Created by Xuefeng Huang on 2020/1/28. // +#include "include/Deck.h" #include "include/Card.h" +#include "include/library.h" Card::Card(){this->card = "empty";} Card::Card(string card,int card_num_in_deck){ @@ -17,25 +19,24 @@ Card::Card(string card){ this->card_number_in_deck = -1; } -bool Card::empty(){ - if(this->card == "empty")return true; - else return false; +bool Card::empty() const { + return this->card == "empty"; } -string Card::getCard() { +string Card::getCard() const { return this->card; } -int Card::getCardInt() { +int Card::getCardInt() const { return this->card_int; } -int Card::getNumberInDeckInt(){ +int Card::getNumberInDeckInt() const { if(this->card_number_in_deck == -1)throw runtime_error("card number in deck cannot be -1"); return this->card_number_in_deck; } -int Card::card2int(Card card) { +int Card::card2int(const Card& card) { return strCard2int(card.getCard()); } @@ -62,11 +63,11 @@ uint64_t Card::boardCards2long(vector cards) { return Card::boardCards2long(cards_objs); } -uint64_t Card::boardCard2long(Card& card){ +uint64_t Card::boardCard2long(const Card& card){ return Card::boardInt2long(card.getCardInt()); } -uint64_t Card::boardCards2long(vector& cards){ +uint64_t Card::boardCards2long(const vector& cards){ std::vector board_int(cards.size()); for(std::size_t i = 0;i < cards.size();i++){ board_int[i] = Card::card2int(cards[i]); @@ -74,7 +75,7 @@ uint64_t Card::boardCards2long(vector& cards){ return Card::boardInts2long(board_int); } -QString Card::boardCards2html(vector& cards){ +QString Card::boardCards2html(const vector& cards){ QString ret_html = ""; for(auto one_card:cards){ if(one_card.empty())continue; @@ -130,21 +131,22 @@ vector Card::long2board(uint64_t board_long) { return board; } -vector Card::long2boardCards(uint64_t board_long){ - vector board = long2board(board_long); - vector board_cards(board.size()); - for(std::size_t i = 0;i < board.size();i ++){ - int one_board = board[i]; - board_cards[i] = Card(intCard2Str(one_board)); +vector Card::long2boardCards(uint64_t board_long, const Deck& deck){ + vector board_ints = long2board(board_long); + vector board_cards; + board_cards.reserve(board_ints.size()); + + for(int card_int : board_ints) { + // This lookup is necessary to get the fully-initialized card object + // from the deck, avoiding the "detached card" problem. + for(const Card& deck_card : deck.getCards()) { + if (deck_card.getCardInt() == card_int) { + board_cards.push_back(deck_card); + break; + } + } } - if (board_cards.size() < 1 || board_cards.size() > 7){ - throw runtime_error(tfm::format("board length not correct, board length %s",board_cards.size())); - } - vector retval(board_cards.size()); - for(std::size_t i = 0;i < board_cards.size();i ++){ - retval[i] = board_cards[i]; - } - return retval; + return board_cards; } string Card::suitToString(int suit) @@ -217,28 +219,31 @@ vector Card::getSuits(){ return {"c","d","h","s"}; } -string Card::toString() { +string Card::toString() const { return this->card; } -string Card::toFormattedString() { +string Card::toFormattedString() const { QString qString = QString::fromStdString(this->card); - qString = qString.replace("c", "♣️"); - qString = qString.replace("d", "♦️"); - qString = qString.replace("h", "♥️"); - qString = qString.replace("s", "♠️"); + // The original implementation was buggy as replace() returns a copy. + if (qString.endsWith('c')) return (qString.chopped(1) + QStringLiteral("♣️")).toStdString(); + if (qString.endsWith('d')) return (qString.chopped(1) + QStringLiteral("♦️")).toStdString(); + if (qString.endsWith('h')) return (qString.chopped(1) + QStringLiteral("♥️")).toStdString(); + if (qString.endsWith('s')) return (qString.chopped(1) + QStringLiteral("♠️")).toStdString(); return qString.toStdString(); } -QString Card::toFormattedHtml() { +QString Card::toFormattedHtml() const { QString qString = QString::fromStdString(this->card); - if(qString.contains("c")) - qString = qString.replace("c", QString::fromLocal8Bit("♣<\/span>")); - else if(qString.contains("d")) - qString = qString.replace("d", QString::fromLocal8Bit("♦<\/span>")); - else if(qString.contains("h")) - qString = qString.replace("h", QString::fromLocal8Bit("♥<\/span>")); - else if(qString.contains("s")) - qString = qString.replace("s", QString::fromLocal8Bit("♠<\/span>")); + // The original implementation was buggy as replace() returns a copy and the else-if was fragile. + if (qString.isEmpty()) return qString; + + QString rank = qString.left(qString.length() - 1); + QChar suit = qString.at(qString.length() - 1); + + if(suit == QLatin1Char('c')) return rank + QLatin1String(""); + if(suit == QLatin1Char('d')) return rank + QLatin1String(""); + if(suit == QLatin1Char('h')) return rank + QLatin1String(""); + if(suit == QLatin1Char('s')) return rank + QLatin1String(""); return qString; } diff --git a/src/Deck.cpp b/src/Deck.cpp index 3301a1e2..a943c190 100644 --- a/src/Deck.cpp +++ b/src/Deck.cpp @@ -19,8 +19,3 @@ Deck::Deck(const vector& ranks, const vector& suits) { } } } - -vector& Deck::getCards() { - return this->cards; -} - diff --git a/src/compairer/Dic5Compairer.cpp b/src/compairer/Dic5Compairer.cpp index 8410ea8e..eb20600f 100644 --- a/src/compairer/Dic5Compairer.cpp +++ b/src/compairer/Dic5Compairer.cpp @@ -5,11 +5,11 @@ #include "include/compairer/Dic5Compairer.h" #include +#include #include #include #include #include "time.h" -#include "unistd.h" #define SUIT_0_MASK 0x1111111111111 #define SUIT_1_MASK 0x2222222222222 @@ -51,65 +51,61 @@ void FiveCardsStrength::convert(unordered_map& strength_map) { } } bool FiveCardsStrength::load(const char* file_path) { - //ifstream file(file_path, ios::binary); - /*if (!file) { - file.close(); - return false; - }*/ - QFile file(QString::fromStdString(file_path)); - if (!file.open(QIODevice::ReadOnly)){ - throw runtime_error("unable to load compairer file"); + if (!file.open(QIODevice::ReadOnly)) { + return false; } + + QDataStream in(&file); + in.setByteOrder(QDataStream::LittleEndian); + flush_map.clear(); other_map.clear(); - int size_key = sizeof(uint64_t), size_int = sizeof(int), val, cnt = 0; - uint64_t key = 0; - char* p_key = (char*)&key, * p_val = (char*)&val, * p_cnt = (char*)&cnt; - file.read(p_cnt, size_int);// 读取行数 - for (int i = 0; i < cnt; i++) { - file.read(p_key, size_key); - file.read(p_val, size_int); - flush_map[key] = val; + + qint32 flush_map_size; + in >> flush_map_size; + if (in.status() != QDataStream::Ok) return false; + + for (qint32 i = 0; i < flush_map_size; ++i) { + quint64 key; + qint32 value; + in >> key >> value; + if (in.status() != QDataStream::Ok) return false; + flush_map[key] = value; } - assert(flush_map.size() == cnt); - file.read(p_cnt, size_int);// 读取行数 - for (int i = 0; i < cnt; i++) { - file.read(p_key, size_key); - file.read(p_val, size_int); - other_map[key] = val; + + qint32 other_map_size; + in >> other_map_size; + if (in.status() != QDataStream::Ok) return false; + + for (qint32 i = 0; i < other_map_size; ++i) { + quint64 key; + qint32 value; + in >> key >> value; + if (in.status() != QDataStream::Ok) return false; + other_map[key] = value; } - assert(other_map.size() == cnt); + file.close(); return true; } bool FiveCardsStrength::save(const char* file_path) { - //qDebug() << "a"; - //sleep(10); - //qDebug() << "b"; - //file_path = "/Users/bytedance/Desktop/card5_dic_zipped_shortdeck.bin"; - ofstream file(file_path, ios::binary); - if (!file) { - file.close(); + QFile file(QString::fromStdString(file_path)); + if (!file.open(QIODevice::WriteOnly)) { return false; } - int size_key = sizeof(uint64_t), size_int = sizeof(int), val = flush_map.size(); - uint64_t key = 0; - char* p_key = (char*)&key, * p_val = (char*)&val; - file.write(p_val, size_int);// 写入行数 - auto it = flush_map.begin(), it_end = flush_map.end(); - for (; it != it_end; it++) { - key = it->first; val = it->second; - file.write(p_key, size_key); - file.write(p_val, size_int); + QDataStream out(&file); + out.setByteOrder(QDataStream::LittleEndian); + + out << static_cast(flush_map.size()); + for (auto const& [key, val] : flush_map) { + out << static_cast(key) << static_cast(val); } - val = other_map.size(); - file.write(p_val, size_int);// 写入行数 - it = other_map.begin(), it_end = other_map.end(); - for (; it != it_end; it++) { - key = it->first; val = it->second; - file.write(p_key, size_key); - file.write(p_val, size_int); + + out << static_cast(other_map.size()); + for (auto const& [key, val] : other_map) { + out << static_cast(key) << static_cast(val); } + file.close(); return true; } @@ -198,26 +194,26 @@ Compairer::CompairResult Dic5Compairer::compairRanks(int rank_former, int rank_l } } -Compairer::CompairResult -Dic5Compairer::compair(vector private_former, vector private_latter, vector public_board) { +template +Compairer::CompairResult Dic5Compairer::compair_template(const vector& private_former, const vector& private_latter, const vector& public_board) { if(private_former.size() != 2) throw runtime_error( tfm::format("private former size incorrect,excepted 2, actually %s",private_former.size()) - ); + ); if(private_latter.size() != 2) throw runtime_error( tfm::format("private latter size incorrect,excepted 2, actually %s",private_latter.size()) ); if(public_board.size() != 5) throw runtime_error( - tfm::format("public board size incorrect,excepted 2, actually %s",public_board.size()) + tfm::format("public board size incorrect,excepted 5, actually %s",public_board.size()) ); - vector former_cards(private_former); + vector former_cards(private_former); former_cards.insert(former_cards.end(),public_board.begin(),public_board.end()); int rank_former = this->getRank(former_cards); - vector latter_cards(private_latter); + vector latter_cards(private_latter); latter_cards.insert(latter_cards.end(),public_board.begin(),public_board.end()); int rank_latter = this->getRank(latter_cards); @@ -225,29 +221,13 @@ Dic5Compairer::compair(vector private_former, vector private_latter, } Compairer::CompairResult -Dic5Compairer::compair(vector private_former, vector private_latter, vector public_board) { - if(private_former.size() != 2) - throw runtime_error( - tfm::format("private former size incorrect,excepted 2, actually %s",private_former.size()) - ); - if(private_latter.size() != 2) - throw runtime_error( - tfm::format("private latter size incorrect,excepted 2, actually %s",private_latter.size()) - ); - if(public_board.size() != 5) - throw runtime_error( - tfm::format("public board size incorrect,excepted 2, actually %s",public_board.size()) - ); - - vector former_cards(private_former); - former_cards.insert(former_cards.end(),public_board.begin(),public_board.end()); - int rank_former = this->getRank(former_cards); - - vector latter_cards(private_latter); - latter_cards.insert(latter_cards.end(),public_board.begin(),public_board.end()); - int rank_latter = this->getRank(latter_cards); +Dic5Compairer::compair(vector private_former, vector private_latter, vector public_board) { + return compair_template(private_former, private_latter, public_board); +} - return this->compairRanks(rank_former,rank_latter); +Compairer::CompairResult +Dic5Compairer::compair(vector private_former, vector private_latter, vector public_board) { + return compair_template(private_former, private_latter, public_board); } int Dic5Compairer::getRank(vector cards) { @@ -287,4 +267,3 @@ int Dic5Compairer::get_rank(vector private_hand, vector public_board) int Dic5Compairer::get_rank(uint64_t private_hand, uint64_t public_board) { return this->get_rank(Card::long2board(private_hand),Card::long2board(public_board)); } - diff --git a/src/console.cpp b/src/console.cpp index d83e852a..ea6d2129 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -3,32 +3,43 @@ // #include "include/tools/CommandLineTool.h" #include "include/tools/argparse.hpp" +#include -int main_backup(int argc,const char **argv) { - ArgumentParser parser; +int main(int argc,const char **argv) { + try { + ArgumentParser parser; - parser.addArgument("-i", "--input_file", 1, true); - parser.addArgument("-r", "--resource_dir", 1, true); - parser.addArgument("-m", "--mode", 1, true); + parser.addArgument("-i", "--input_file", 1, true); + parser.addArgument("-r", "--resource_dir", 1, true); + parser.addArgument("-m", "--mode", 1, true); - parser.parse(argc, argv); + parser.parse(argc, argv); - string input_file = parser.retrieve("input_file"); - string resource_dir = parser.retrieve("resource_dir"); - if(resource_dir.empty()){ - resource_dir = "./resources"; - } - string mode = parser.retrieve("mode"); - if(mode.empty()){mode = "holdem";} - if(mode != "holdem" && mode != "shortdeck") - throw runtime_error(tfm::format("mode %s error, not in ['holdem','shortdeck']",mode)); + string input_file = parser.retrieve("input_file"); + string resource_dir = parser.retrieve("resource_dir"); + if(resource_dir.empty()){ + resource_dir = "./resources"; + } + string mode = parser.retrieve("mode"); + if(mode.empty()){mode = "holdem";} + if(mode != "holdem" && mode != "shortdeck") + throw runtime_error(tfm::format("mode %s error, not in ['holdem','shortdeck']",mode)); - if(input_file.empty()) { - CommandLineTool clt = CommandLineTool(mode,resource_dir); - clt.startWorking(); - }else{ - cout << "EXEC FROM FILE" << endl; - CommandLineTool clt = CommandLineTool(mode,resource_dir); - clt.execFromFile(input_file); + if(input_file.empty()) { + CommandLineTool clt = CommandLineTool(mode,resource_dir); + clt.startWorking(); + }else{ + cout << "EXEC FROM FILE" << endl; + CommandLineTool clt = CommandLineTool(mode,resource_dir); + clt.execFromFile(input_file); + } + } catch (const std::runtime_error& e) { + cerr << "A runtime error occurred: " << e.what() << endl; + cerr << "This might be due to missing resource files. Please check your --resource_dir path." << endl; + return 1; + } catch (...) { + cerr << "An unknown error occurred." << endl; + return 1; } + return 0; } diff --git a/src/nodes/GameActions.cpp b/src/nodes/GameActions.cpp index 920a9be7..82238d04 100644 --- a/src/nodes/GameActions.cpp +++ b/src/nodes/GameActions.cpp @@ -6,11 +6,11 @@ GameActions::GameActions() = default; -GameTreeNode::PokerActions GameActions::getAction() { +GameTreeNode::PokerActions GameActions::getAction() const { return this->action; } -double GameActions::getAmount() { +double GameActions::getAmount() const { return this->amount; } @@ -24,7 +24,7 @@ GameActions::GameActions(GameTreeNode::PokerActions action, double amount) { this->amount = amount; } -string GameActions::pokerActionToString(GameTreeNode::PokerActions pokerActions) { +string GameActions::pokerActionToString(GameTreeNode::PokerActions pokerActions) const { switch (pokerActions) { case GameTreeNode::PokerActions::BEGIN : return "BEGIN"; @@ -40,7 +40,7 @@ string GameActions::pokerActionToString(GameTreeNode::PokerActions pokerActions) } } -string GameActions::toString() { +string GameActions::toString() const { if(this->amount == -1) { return this->pokerActionToString(this->action); }else{ diff --git a/src/ranges/PrivateCards.cpp b/src/ranges/PrivateCards.cpp index 5c450cb3..e2afaa89 100644 --- a/src/ranges/PrivateCards.cpp +++ b/src/ranges/PrivateCards.cpp @@ -21,17 +21,17 @@ PrivateCards::PrivateCards(int card1, int card2, float weight) { this->board_long = Card::boardInts2long(this->card_vec); } -uint64_t PrivateCards::toBoardLong() { +uint64_t PrivateCards::toBoardLong() const { return this->board_long; //return Card::boardInts2long(this->card_vec); } -int PrivateCards::hashCode() { +int PrivateCards::hashCode() const { return this->hash_code; } -string PrivateCards::toString() { +string PrivateCards::toString() const { if (card1 > card2) { return Card::intCard2Str(card1) + Card::intCard2Str(card2); }else{ diff --git a/src/runtime/PokerSolver.cpp b/src/runtime/PokerSolver.cpp index bff1619c..4ed7a7ad 100644 --- a/src/runtime/PokerSolver.cpp +++ b/src/runtime/PokerSolver.cpp @@ -84,19 +84,38 @@ long long PokerSolver::estimate_tree_memory(QString range1,QString range2,QStrin vector range1 = PrivateRangeConverter::rangeStr2Cards(player1RangeStr,initialBoard); vector range2 = PrivateRangeConverter::rangeStr2Cards(player2RangeStr,initialBoard); - return this->game_tree->estimate_tree_memory(this->deck.getCards().size() - initialBoard.size(),range1.size(),range2.size()); + return this->game_tree->estimate_tree_memory(static_cast(this->deck.getCards().size() - initialBoard.size()), + static_cast(range1.size()), + static_cast(range2.size())); } } void PokerSolver::train(string p1_range, string p2_range, string boards, string log_file, int iteration_number, int print_interval, string algorithm,int warmup,float accuracy,bool use_isomorphism, int use_halffloats, int threads) { + if (this->game_tree == nullptr) { + throw runtime_error("Game tree not built. Please use the build_tree command first."); + } + if (boards.empty()) { + throw runtime_error("Board is empty. Please use the set_board command first."); + } string player1RangeStr = p1_range; string player2RangeStr = p2_range; vector board_str_arr = string_split(boards,','); vector initialBoard; - for(string one_board_str:board_str_arr){ - initialBoard.push_back(Card::strCard2int(one_board_str)); + + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + if (board_str_arr.size() < 3) { + throw runtime_error("Hand analysis requires at least a 3-card flop."); + } + // For hand analysis, the initial board for the tree is just the flop + for(int i = 0; i < 3; ++i) { + initialBoard.push_back(Card::strCard2int(board_str_arr[i])); + } + } else { + for(const string& one_board_str:board_str_arr){ + initialBoard.push_back(Card::strCard2int(one_board_str)); + } } vector range1 = PrivateRangeConverter::rangeStr2Cards(player1RangeStr,initialBoard); @@ -125,21 +144,24 @@ void PokerSolver::train(string p1_range, string p2_range, string boards, string , use_isomorphism , use_halffloats , threads + , this->analysis_mode + , this->full_board ); this->solver->train(); } -void PokerSolver::dump_strategy(QString dump_file,int dump_rounds) { - //locale &loc=locale::global(locale(locale(),"",LC_CTYPE)); +void PokerSolver::dump_strategy(QString dump_file, int dump_rounds) { setlocale(LC_ALL,""); - json dump_json = this->solver->dumps(false,dump_rounds); - //QFile ofile( QString::fromStdString(dump_file)); ofstream fileWriter; + const size_t buffer_size = 4 * 1024 * 1024; // 4MB buffer + std::vector buffer(buffer_size); + fileWriter.open(dump_file.toLocal8Bit()); + if(!fileWriter.fail()){ - fileWriter << dump_json; - fileWriter.flush(); + fileWriter.rdbuf()->pubsetbuf(buffer.data(), buffer_size); + this->solver->dumps(fileWriter, false, dump_rounds); fileWriter.close(); qDebug().noquote() << QObject::tr("save success"); }else{ diff --git a/src/runtime/qsolverjob.cpp b/src/runtime/qsolverjob.cpp index 27a663f8..f7e3ed88 100644 --- a/src/runtime/qsolverjob.cpp +++ b/src/runtime/qsolverjob.cpp @@ -96,10 +96,11 @@ void QSolverJob::stop(){ void QSolverJob::solving(){ // TODO 为什么ui上多次求解会积累memory?哪里leak了? - // TODO 为什么有时候会莫名闪退? qDebug().noquote() << tr("Start Solving..");//.toStdString() << std::endl; if(this->mode == Mode::HOLDEM){ + this->ps_holdem.analysis_mode = static_cast(this->analysis_mode); + this->ps_holdem.full_board = this->full_board; this->ps_holdem.train( this->range_ip, this->range_oop, @@ -115,6 +116,8 @@ void QSolverJob::solving(){ this->thread_number ); }else if(this->mode == Mode::SHORTDECK){ + this->ps_shortdeck.analysis_mode = static_cast(this->analysis_mode); + this->ps_shortdeck.full_board = this->full_board; this->ps_shortdeck.train( this->range_ip, this->range_oop, diff --git a/src/solver/BestResponse.cpp b/src/solver/BestResponse.cpp index 90d174b4..925142da 100644 --- a/src/solver/BestResponse.cpp +++ b/src/solver/BestResponse.cpp @@ -9,8 +9,10 @@ //#define DEBUG; BestResponse::BestResponse(vector> &private_combos, int player_number, - PrivateCardsManager &pcm, RiverRangeManager &rrm, Deck &deck, bool debug,int color_iso_offset[][4],GameTreeNode::GameRound split_round,int nthreads, int use_halffloats) + PrivateCardsManager &pcm, RiverRangeManager &rrm, Deck &deck, bool debug,int color_iso_offset[][4], + GameTreeNode::GameRound split_round,int nthreads, int use_halffloats, Solver::AnalysisMode analysis_mode, const vector& full_board_cards) :rrm(rrm),pcm(pcm),private_combos(private_combos),deck(deck){ + this->analysis_mode = analysis_mode; this->player_number = player_number; this->debug = debug; @@ -24,6 +26,10 @@ BestResponse::BestResponse(vector> &private_combos, int pla for(int i = 0;i < player_number;i ++) { player_hands[i] = private_combos[i].size(); } + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + this->full_board_cards = full_board_cards; + this->full_board_long = Card::boardCards2long(this->full_board_cards); + } this->nthreads = nthreads; this->use_halffloats = use_halffloats; for(int i = 0;i < 52 * 52 * 2;i ++){ @@ -115,7 +121,56 @@ vector BestResponse::bestResponse(shared_ptr node, int play vector BestResponse::chanceBestReponse(shared_ptr node, int player,const vector>& reach_probs, uint64_t current_board, int deal) { - vector& cards = this->deck.getCards(); + const vector& cards = this->deck.getCards(); + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + GameTreeNode::GameRound round = node->getRound(); + Card next_card; + if (round == GameTreeNode::GameRound::TURN) { + if (full_board_cards.size() < 4) throw runtime_error("Hand analysis requires at least 4 board cards for flop->turn."); + next_card = full_board_cards[3]; + } else if (round == GameTreeNode::GameRound::RIVER) { + if (full_board_cards.size() < 5) throw runtime_error("Hand analysis requires 5 board cards for turn->river."); + next_card = full_board_cards[4]; + } else { + throw runtime_error("Hand analysis mode is only for post-flop chance nodes."); + } + + int card_idx = next_card.getNumberInDeckInt(); + uint64_t card_long = Card::boardInt2long(next_card.getCardInt()); + uint64_t new_board_long = current_board | card_long; + + vector> new_reach_probs(2); + new_reach_probs[player] = vector(this->pcm.getPreflopCards(player).size()); + new_reach_probs[1 - player] = vector(this->pcm.getPreflopCards(1 - player).size()); + + int possible_deals = node->getCards().size() - Card::long2board(current_board).size() - 2; + + for (int one_player = 0; one_player < 2; one_player++) { + for (size_t hand_idx = 0; hand_idx < this->pcm.getPreflopCards(one_player).size(); hand_idx++) { + uint64_t privateBoardLong = this->pcm.getPreflopCards(one_player)[hand_idx].toBoardLong(); + if (Card::boardsHasIntercept(card_long, privateBoardLong)) { + new_reach_probs[one_player][hand_idx] = 0; + } else { + new_reach_probs[one_player][hand_idx] = reach_probs[one_player][hand_idx]; + } + } + } + + int new_deal; + int card_num = this->deck.getCards().size(); + if (deal == 0) { + new_deal = card_idx + 1; + } else if (deal > 0 && deal <= card_num) { + int origin_deal = deal - 1; + new_deal = card_num * origin_deal + card_idx; + new_deal += (1 + card_num); + } else { + // This case (dealing a 3rd card post-flop) shouldn't happen in Texas Hold'em. + throw runtime_error("Hand analysis with more than two dealt cards is not supported."); + } + + return this->bestResponse(node->getChildren(), player, new_reach_probs, new_board_long, new_deal); + } int card_num = node->getCards().size(); // 可能的发牌情况,2代表每个人的holecard是两张 @@ -130,7 +185,7 @@ BestResponse::chanceBestReponse(shared_ptr node, int player,const ve vector> results(node->getCards().size()); #pragma omp parallel for - for(std::size_t card = 0;card < node->getCards().size();card ++) { + for(int card = 0; card < static_cast(node->getCards().size()); card++) { shared_ptr one_child = node->getChildren(); Card one_card = node->getCards()[card]; uint64_t card_long = Card::boardInt2long(one_card.getCardInt()); @@ -330,57 +385,56 @@ BestResponse::actionBestResponse(shared_ptr node, int player, const vector BestResponse::terminalBestReponse(shared_ptr node, int player, const vector>& reach_probs, uint64_t board, int deal) { - uint64_t board_long = board; int oppo = 1 - player; - const vector& player_combs = this->rrm.getRiverCombos(player,this->pcm.getPreflopCards(player),board); //this.river_combos[player]; - const vector& oppo_combs = this->rrm.getRiverCombos(1 - player,this->pcm.getPreflopCards(1 - player),board); //this.river_combos[player]; - float player_payoff = node->get_payoffs()[player]; - vector payoffs = vector(this->player_hands[player]); + const vector& player_hands_vec = this->private_combos[player]; + const vector& oppo_hands_vec = this->private_combos[oppo]; + const vector& oppo_reach_prob = reach_probs[oppo]; + + vector payoffs(player_hands_vec.size()); #ifdef DEBUG if(this->player_number != 2) throw runtime_error("player NE 2 not supported"); #endif // 对手的手牌可能需要和其reach prob一样长 - vector oppo_card_sum(52); + vector oppo_card_sum(52, 0.0f); //用于记录对手总共的手牌绝对prob之和 float oppo_prob_sum = 0; - const vector& oppo_reach_prob = reach_probs[1 - player]; - for(std::size_t oppo_hand = 0;oppo_hand < oppo_combs.size(); oppo_hand ++){ - const RiverCombs& one_hc = oppo_combs[oppo_hand]; - uint64_t one_hc_long = Card::boardInts2long(one_hc.private_cards.get_hands()); + for(size_t i = 0; i < oppo_hands_vec.size(); ++i) { + const PrivateCards& oppo_hc = oppo_hands_vec[i]; + uint64_t one_hc_long = oppo_hc.toBoardLong(); // 如果对手手牌和public card有重叠,那么这组牌不可能存在 - if(Card::boardsHasIntercept(one_hc_long,board_long)){ + if(Card::boardsHasIntercept(one_hc_long, board)){ continue; } - oppo_prob_sum += oppo_reach_prob[one_hc.reach_prob_index]; - oppo_card_sum[one_hc.private_cards.card1] += oppo_reach_prob[one_hc.reach_prob_index]; - oppo_card_sum[one_hc.private_cards.card2] += oppo_reach_prob[one_hc.reach_prob_index]; + oppo_prob_sum += oppo_reach_prob[i]; + oppo_card_sum[oppo_hc.card1] += oppo_reach_prob[i]; + oppo_card_sum[oppo_hc.card2] += oppo_reach_prob[i]; } - for(std::size_t player_hand = 0;player_hand < player_combs.size();player_hand ++) { - const RiverCombs& player_hc = player_combs[player_hand]; - uint64_t player_hc_long = Card::boardInts2long(player_hc.private_cards.get_hands()); - if(Card::boardsHasIntercept(player_hc_long,board_long)){ - payoffs[player_hand] = 0; + for(size_t i = 0; i < player_hands_vec.size(); ++i) { + const PrivateCards& player_hc = player_hands_vec[i]; + uint64_t player_hc_long = player_hc.toBoardLong(); + if(Card::boardsHasIntercept(player_hc_long, board)){ + payoffs[i] = 0; }else{ - int oppo_hand = this->pcm.indPlayer2Player(player,oppo,player_hc.reach_prob_index); + int oppo_hand = this->pcm.indPlayer2Player(player,oppo,i); float add_reach_prob; if(oppo_hand == -1){ add_reach_prob = 0; }else{ add_reach_prob = oppo_reach_prob[oppo_hand]; } - payoffs[player_hc.reach_prob_index] = (oppo_prob_sum - - oppo_card_sum[player_hc.private_cards.card1] - - oppo_card_sum[player_hc.private_cards.card2] + payoffs[i] = (oppo_prob_sum + - oppo_card_sum[player_hc.card1] + - oppo_card_sum[player_hc.card2] + add_reach_prob ) * player_payoff; } @@ -437,6 +491,7 @@ BestResponse::showdownBestResponse(shared_ptr node, int player,con // 计算失败时的payoff float losssum = 0; vector card_losssum(52); + fill(card_losssum.begin(), card_losssum.end(), 0); j = oppo_combs.size() - 1; for(int i = player_combs.size() - 1;i >= 0;i --){ @@ -461,4 +516,3 @@ BestResponse::showdownBestResponse(shared_ptr node, int player,con } return payoffs; } - diff --git a/src/solver/PCfrSolver.cpp b/src/solver/PCfrSolver.cpp index 29395eda..0258d62b 100644 --- a/src/solver/PCfrSolver.cpp +++ b/src/solver/PCfrSolver.cpp @@ -15,14 +15,30 @@ PCfrSolver::~PCfrSolver(){ } PCfrSolver::PCfrSolver(shared_ptr tree, vector range1, vector range2, - vector initial_board, shared_ptr compairer, Deck deck, int iteration_number, bool debug, - int print_interval, string logfile, string trainer, Solver::MonteCarolAlg monteCarolAlg,int warmup,float accuracy,bool use_isomorphism,int use_halffloats,int num_threads) :Solver(tree){ + vector initial_board, shared_ptr compairer, Deck deck, int iteration_number, + bool debug, int print_interval, string logfile, string trainer, Solver::MonteCarolAlg monteCarolAlg, + int warmup,float accuracy,bool use_isomorphism,int use_halffloats,int num_threads, + Solver::AnalysisMode analysis_mode, const string& full_board) :Solver(tree){ this->initial_board = initial_board; - this->initial_board_long = Card::boardInts2long(initial_board); + this->initial_board_long = Card::boardInts2long(this->initial_board); this->logfile = logfile; this->trainer = trainer; this->warmup = warmup; + for (int card_int : this->initial_board) { + bool found = false; + for (const Card& deck_card : deck.getCards()) { + if (deck_card.getCardInt() == card_int) { + this->initial_board_cards.push_back(deck_card); + found = true; + break; + } + } + if (!found) { + throw runtime_error("Card from initial board not found in deck: " + Card::intCard2Str(card_int)); + } + } + range1 = this->noDuplicateRange(range1,initial_board_long); range2 = this->noDuplicateRange(range2,initial_board_long); @@ -55,6 +71,28 @@ PCfrSolver::PCfrSolver(shared_ptr tree, vector range1, v } qDebug().noquote() << QString::fromStdString(tfm::format(QObject::tr("Using %s threads").toStdString().c_str(),num_threads)); this->num_threads = num_threads; + this->analysis_mode = analysis_mode; + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + if (full_board.empty()) { + throw runtime_error("Hand analysis mode requires a full board, but the board string is empty."); + } + vector card_strs = string_split(full_board, ','); + for (const string& one_card_str : card_strs) { + if (one_card_str.empty()) continue; + bool found = false; + for(const Card& deck_card : this->deck.getCards()){ + if(deck_card.getCard() == one_card_str){ + this->full_board_cards.push_back(deck_card); + found = true; + break; + } + } + if(!found){ + throw runtime_error("Card from board string not found in deck: " + one_card_str); + } + } + this->full_board_long = Card::boardCards2long(this->full_board_cards); + } this->distributing_task = false; omp_set_num_threads(this->num_threads); setTrainable(this->tree->getRoot()); @@ -167,13 +205,10 @@ vector PCfrSolver::getAllAbstractionDeal(int deal){ } else if (deal > 0 && deal <= card_num){ int origin_deal = int((deal - 1) / 4) * 4; for(int i = 0;i < 4;i ++){ - int one_card = origin_deal + i + 1; - - Card *first_card = const_cast(&(this->deck.getCards()[origin_deal + i])); - uint64_t first_long = Card::boardInt2long( - first_card->getCardInt()); + const Card& first_card = this->deck.getCards()[origin_deal + i]; + uint64_t first_long = Card::boardInt2long(first_card.getCardInt()); if (Card::boardsHasIntercept(first_long, this->initial_board_long))continue; - all_deal.push_back(one_card); + all_deal.push_back(origin_deal + i + 1); } } else{ //cout << "______________________" << endl; @@ -184,17 +219,12 @@ vector PCfrSolver::getAllAbstractionDeal(int deal){ for(int i = 0;i < 4;i ++) { for(int j = 0;j < 4;j ++) { if(first_deal == second_deal && i == j) continue; - - Card *first_card = const_cast(&(this->deck.getCards()[first_deal + i])); - uint64_t first_long = Card::boardInt2long( - first_card->getCardInt()); + const Card& first_card = this->deck.getCards()[first_deal + i]; + uint64_t first_long = Card::boardInt2long(first_card.getCardInt()); if (Card::boardsHasIntercept(first_long, this->initial_board_long))continue; - - Card *second_card = const_cast(&(this->deck.getCards()[second_deal + j])); - uint64_t second_long = Card::boardInt2long( - second_card->getCardInt()); + const Card& second_card = this->deck.getCards()[second_deal + j]; + uint64_t second_long = Card::boardInt2long(second_card.getCardInt()); if (Card::boardsHasIntercept(second_long, this->initial_board_long))continue; - int one_card = card_num * (first_deal + i) + (second_deal + j) + 1 + card_num; //cout << ";" << this->deck.getCards()[first_deal + i].toString() << "," << this->deck.getCards()[second_deal + j].toString(); all_deal.push_back(one_card); @@ -228,7 +258,53 @@ vector PCfrSolver::cfr(int player, shared_ptr node, const v vector PCfrSolver::chanceUtility(int player, shared_ptr node, const vector &reach_probs, int iter, uint64_t current_board,int deal) { - vector& cards = this->deck.getCards(); + const vector& cards = this->deck.getCards(); + + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + GameTreeNode::GameRound round = node->getRound(); + Card next_card; + if (round == GameTreeNode::GameRound::TURN) { + if (full_board_cards.size() < 4) throw runtime_error("Hand analysis requires at least 4 board cards for flop->turn."); + next_card = full_board_cards[3]; + } else if (round == GameTreeNode::GameRound::RIVER) { + if (full_board_cards.size() < 5) throw runtime_error("Hand analysis requires 5 board cards for turn->river."); + next_card = full_board_cards[4]; + } else { + throw runtime_error("Hand analysis mode is only for post-flop chance nodes."); + } + + int card_idx = next_card.getNumberInDeckInt(); + uint64_t card_long = Card::boardInt2long(next_card.getCardInt()); + uint64_t new_board_long = current_board | card_long; + + vector new_reach_probs = vector(this->ranges[1 - player].size()); + int possible_deals = node->getCards().size() - Card::long2board(current_board).size() - 2; + int oppo = 1 - player; + + for (size_t player_hand = 0; player_hand < this->ranges[oppo].size(); player_hand++) { + PrivateCards &one_private = this->ranges[oppo][player_hand]; + if (Card::boardsHasIntercept(card_long, one_private.toBoardLong())) { + new_reach_probs[player_hand] = 0; + } else { + new_reach_probs[player_hand] = reach_probs[player_hand]; + } + } + + int new_deal; + int card_num = this->deck.getCards().size(); + if (deal == 0) { + new_deal = card_idx + 1; + } else if (deal > 0 && deal <= card_num) { + int origin_deal = deal - 1; + new_deal = card_num * origin_deal + card_idx; + new_deal += (1 + card_num); + } else { + // This case (dealing a 3rd card post-flop) shouldn't happen in Texas Hold'em. + throw runtime_error("Hand analysis with more than two dealt cards is not supported."); + } + return this->cfr(player, node->getChildren(), new_reach_probs, iter, new_board_long, new_deal); + } + //float[] cardWeights = getCardsWeights(player,reach_probs[1 - player],current_board); int card_num = node->getCards().size(); @@ -265,20 +341,10 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< int multiplier_num = 0; for (int i = 0; i < 4; i++) { int i_card = card_base * 4 + i; - if (i == cardr) { - Card *one_card = const_cast(&(node->getCards()[i_card])); - uint64_t card_long = Card::boardInt2long( - one_card->getCardInt()); - if (!Card::boardsHasIntercept(card_long, current_board)) { - multiplier_num += 1; - } - } else { - Card *one_card = const_cast(&(node->getCards()[i_card])); - uint64_t card_long = Card::boardInt2long( - one_card->getCardInt()); - if (!Card::boardsHasIntercept(card_long, current_board)) { - multiplier_num += 1; - } + const Card& one_card = node->getCards()[i_card]; + uint64_t card_long = Card::boardInt2long(one_card.getCardInt()); + if (!Card::boardsHasIntercept(card_long, current_board)) { + multiplier_num += 1; } } multiplier[card_target] = multiplier_num; @@ -290,20 +356,20 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< for(std::size_t card = 0;card < node->getCards().size();card ++) { shared_ptr one_child = node->getChildren(); - Card *one_card = const_cast(&(node->getCards()[card])); - uint64_t card_long = Card::boardInt2long(one_card->getCardInt());//Card::boardCards2long(new Card[]{one_card}); + const Card& one_card = node->getCards()[card]; + uint64_t card_long = Card::boardInt2long(one_card.getCardInt());//Card::boardCards2long(new Card[]{one_card}); if (Card::boardsHasIntercept(card_long, current_board)) continue; if (iter <= this->warmup && multiplier[card] == 0) continue; - if (this->color_iso_offset[deal][one_card->getCardInt() % 4] < 0) continue; + if (this->color_iso_offset[deal][one_card.getCardInt() % 4] < 0) continue; valid_cards.push_back(card); } #pragma omp parallel for schedule(static) - for(std::size_t valid_ind = 0;valid_ind < valid_cards.size();valid_ind++) { - int card = valid_cards[valid_ind]; + for(int valid_ind = 0; valid_ind < static_cast(valid_cards.size()); valid_ind++) { + int card = valid_cards[static_cast(valid_ind)]; shared_ptr one_child = node->getChildren(); - Card *one_card = const_cast(&(node->getCards()[card])); - uint64_t card_long = Card::boardInt2long(one_card->getCardInt());//Card::boardCards2long(new Card[]{one_card}); + const Card& one_card = node->getCards()[card]; + uint64_t card_long = Card::boardInt2long(one_card.getCardInt());//Card::boardCards2long(new Card[]{one_card}); uint64_t new_board_long = current_board | card_long; if (this->monteCarolAlg == MonteCarolAlg::PUBLIC) { @@ -355,28 +421,28 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< throw runtime_error(tfm::format("deal out of range : %s ",deal)); } if(this->distributing_task && node->getRound() == this->split_round) { - results[one_card->getNumberInDeckInt()] = vector(this->ranges[player].size()); + results[one_card.getNumberInDeckInt()] = vector(this->ranges[player].size()); //TaskParams taskParams = TaskParams(); }else { vector child_utility = this->cfr(player, one_child, new_reach_probs, iter, new_board_long, new_deal); - results[one_card->getNumberInDeckInt()] = child_utility; + results[one_card.getNumberInDeckInt()] = child_utility; } } for(std::size_t card = 0;card < node->getCards().size();card ++) { - Card *one_card = const_cast(&(node->getCards()[card])); + const Card& one_card = node->getCards()[card]; vector child_utility; - int offset = this->color_iso_offset[deal][one_card->getCardInt() % 4]; + int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; if(offset < 0) { - int rank1 = one_card->getCardInt() % 4; + int rank1 = one_card.getCardInt() % 4; int rank2 = rank1 + offset; #ifdef DEBUG if(rank2 < 0) throw runtime_error("rank error"); #endif - child_utility = results[one_card->getNumberInDeckInt() + offset]; + child_utility = results[one_card.getNumberInDeckInt() + offset]; exchange_color(child_utility,this->pcm.getPreflopCards(player),rank1,rank2); }else{ - child_utility = results[one_card->getNumberInDeckInt()]; + child_utility = results[one_card.getNumberInDeckInt()]; } if(child_utility.empty()) continue; @@ -522,11 +588,7 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< if(!this->distributing_task && !this->collecting_statics) { if (iter > this->warmup) { trainable->updateRegrets(regrets, iter + 1, reach_probs); - }/*else if(iter < this->warmup){ - vector deals = this->getAllAbstractionDeal(deal); - shared_ptr one_trainable = node->getTrainable(deals[0]); - one_trainable->updateRegrets(regrets, iter + 1, reach_probs[player]); - }*/ + } else { // iter == this->warmup vector deals = this->getAllAbstractionDeal(deal); @@ -603,11 +665,11 @@ PCfrSolver::showdownUtility(int player, shared_ptr node, const vec vector payoffs = vector(player_private_cards.size()); - float winsum = 0; + double winsum = 0; vector card_winsum = vector (52);//node->card_sum; fill(card_winsum.begin(),card_winsum.end(),0); - int j = 0; + size_t j = 0; for(std::size_t i = 0;i < player_combs.size();i ++){ const RiverCombs& one_player_comb = player_combs[i]; while (j < oppo_combs.size() && one_player_comb.rank < oppo_combs[j].rank){ @@ -617,28 +679,28 @@ PCfrSolver::showdownUtility(int player, shared_ptr node, const vec card_winsum[one_oppo_comb.private_cards.card2] += reach_probs[one_oppo_comb.reach_prob_index]; j ++; } - payoffs[one_player_comb.reach_prob_index] = (winsum + payoffs[one_player_comb.reach_prob_index] = static_cast(winsum - card_winsum[one_player_comb.private_cards.card1] - card_winsum[one_player_comb.private_cards.card2] ) * win_payoff; } // 计算失败时的payoff - float losssum = 0; + double losssum = 0; vector& card_losssum = card_winsum; fill(card_losssum.begin(),card_losssum.end(),0); - j = oppo_combs.size() - 1; - for(int i = player_combs.size() - 1;i >= 0;i --){ + long long j_rev = static_cast(oppo_combs.size()) - 1; + for(long long i = static_cast(player_combs.size()) - 1;i >= 0;i --){ const RiverCombs& one_player_comb = player_combs[i]; - while (j >= 0 && one_player_comb.rank > oppo_combs[j].rank){ - const RiverCombs& one_oppo_comb = oppo_combs[j]; + while (j_rev >= 0 && one_player_comb.rank > oppo_combs[j_rev].rank){ + const RiverCombs& one_oppo_comb = oppo_combs[j_rev]; losssum += reach_probs[one_oppo_comb.reach_prob_index]; card_losssum[one_oppo_comb.private_cards.card1] += reach_probs[one_oppo_comb.reach_prob_index]; card_losssum[one_oppo_comb.private_cards.card2] += reach_probs[one_oppo_comb.reach_prob_index]; - j --; + j_rev --; } - payoffs[one_player_comb.reach_prob_index] += (losssum + payoffs[one_player_comb.reach_prob_index] += static_cast(losssum - card_losssum[one_player_comb.private_cards.card1] - card_losssum[one_player_comb.private_cards.card2] ) * lose_payoff; @@ -691,7 +753,7 @@ PCfrSolver::terminalUtility(int player, shared_ptr node, const vec void PCfrSolver::findGameSpecificIsomorphisms() { // hand isomorphisms - vector board_cards = Card::long2boardCards(this->initial_board_long); + vector board_cards = Card::long2boardCards(this->initial_board_long, this->deck); for(int i = 0;i <= 1;i ++){ vector& range = i == 0?this->range1:this->range2; for(std::size_t i_range = 0;i_range < range.size();i_range ++) { @@ -775,7 +837,8 @@ void PCfrSolver::train() { this->findGameSpecificIsomorphisms(); } - BestResponse br = BestResponse(player_privates,this->player_number,this->pcm,this->rrm,this->deck,this->debug,this->color_iso_offset,this->split_round,this->num_threads,this->use_halffloats); + BestResponse br = BestResponse(player_privates,this->player_number,this->pcm,this->rrm,this->deck,this->debug,this->color_iso_offset,this->split_round,this->num_threads,this->use_halffloats, + this->analysis_mode, this->full_board_cards); br.printExploitability(tree->getRoot(), 0, tree->getRoot()->getPot(), initial_board_long); @@ -849,164 +912,235 @@ void PCfrSolver::train() { void PCfrSolver::exchangeRange(json& strategy,int rank1,int rank2,shared_ptr one_node){ if(rank1 == rank2)return; + int player = one_node->getPlayer(); - vector range_strs; - vector> strategies; + const vector& range = this->ranges[player]; - for(std::size_t i = 0;i < this->ranges[player].size();i ++){ - string one_range_str = this->ranges[player][i].toString(); - if(!strategy.contains(one_range_str)){ - for(auto one_key:strategy.items()){ - cout << one_key.key() << endl; + // Create a map for efficient lookup of hand indices. This is much faster + // than searching the range vector repeatedly. + unordered_map hand_hash_to_index; + for(size_t i = 0; i < range.size(); ++i) { + hand_hash_to_index[range[i].hashCode()] = i; + } + + vector swapped(range.size(), false); + + for(std::size_t i = 0; i < range.size(); i++){ + if (swapped[i]) { + continue; + } + + const PrivateCards& pc_i = range[i]; + + // Find the isomorphic hand j + PrivateCards pc_j = pc_i.exchange_color(rank1, rank2); + + auto it = hand_hash_to_index.find(pc_j.hashCode()); + + if (it != hand_hash_to_index.end()) { + size_t j = it->second; + if (i < j) { // Ensure we only swap each pair once + string str_i = pc_i.toString(); + string str_j = pc_j.toString(); + if (strategy.contains(str_i) && strategy.contains(str_j)) { + swap(strategy[str_i], strategy[str_j]); + swapped[i] = true; + swapped[j] = true; + } } - cout << "strategy: " << strategy << endl; - throw runtime_error(tfm::format("%s not exist in strategy",one_range_str)); } - vector one_strategy = strategy[one_range_str]; - range_strs.push_back(one_range_str); - strategies.push_back(one_strategy); } - exchange_color(strategies,this->ranges[player],rank1,rank2); +} - for(std::size_t i = 0;i < this->ranges[player].size();i ++) { - string one_range_str = this->ranges[player][i].toString(); - vector one_strategy = strategies[i]; - strategy[one_range_str] = one_strategy; +void PCfrSolver::reConvertJson(std::ostream& stream, const shared_ptr& node, const string& key, int depth, int max_depth, vector prefix, int deal, vector> exchange_color_list) { + if (!key.empty()) { + stream << "\"" << key << "\":"; } -} -void PCfrSolver::reConvertJson(const shared_ptr& node,json& strategy,string key,int depth,int max_depth,vector prefix,int deal,vector> exchange_color_list) { - if(depth >= max_depth) return; - if(node->getType() == GameTreeNode::GameTreeNodeType::ACTION) { - json* retval; - if(key != ""){ - strategy[key] = json(); - retval = &(strategy[key]); - }else{ - retval = &strategy; - } + if(depth >= max_depth) { + stream << "{}"; + return; + } + + stream << "{"; + bool first_property = true; + auto write_comma = [&]() { + if (!first_property) stream << ","; + first_property = false; + }; + + if(node->getType() == GameTreeNode::GameTreeNodeType::ACTION) { shared_ptr one_node = std::dynamic_pointer_cast(node); - vector actions_str; - for(GameActions one_action:one_node->getActions()) actions_str.push_back(one_action.toString()); + write_comma(); + stream << "\"node_type\":\"action_node\""; - (*retval)["actions"] = actions_str; - (*retval)["player"] = one_node->getPlayer(); + write_comma(); + stream << "\"player\":" << one_node->getPlayer(); - (*retval)["childrens"] = json(); - json& childrens = (*retval)["childrens"]; + vector actions_str; + for(GameActions one_action:one_node->getActions()) actions_str.push_back(one_action.toString()); + write_comma(); + stream << "\"actions\":" << json(actions_str); - for(std::size_t i = 0;i < one_node->getActions().size();i ++){ - GameActions& one_action = one_node->getActions()[i]; - shared_ptr one_child = one_node->getChildrens()[i]; - vector new_prefix(prefix); - new_prefix.push_back(one_action.toString()); - this->reConvertJson(one_child,childrens,one_action.toString(),depth,max_depth,new_prefix,deal,exchange_color_list); - } - if((*retval)["childrens"].empty()){ - (*retval).erase("childrens"); - } shared_ptr trainable = one_node->getTrainable(deal,false); if(trainable != nullptr) { - (*retval)["strategy"] = trainable->dump_strategy(false); - for(vector one_exchange:exchange_color_list){ - int rank1 = one_exchange[0]; - int rank2 = one_exchange[1]; - this->exchangeRange((*retval)["strategy"]["strategy"],rank1,rank2,one_node); + write_comma(); + stream << "\"strategy\":"; + trainable->dump_strategy(stream, false, exchange_color_list, one_node); + } + const auto& children = one_node->getChildrens(); + if (!children.empty()) { + write_comma(); + stream << "\"childrens\":{"; + bool first_child = true; + for (size_t i = 0; i < children.size(); ++i) { + if (!first_child) stream << ","; + first_child = false; + const GameActions& one_action = one_node->getActions()[i]; + const auto& one_child = children[i]; + vector new_prefix(prefix); + new_prefix.push_back(one_action.toString()); + this->reConvertJson(stream, one_child, one_action.toString(), depth, max_depth, new_prefix, deal, exchange_color_list); } + stream << "}"; } - (*retval)["node_type"] = "action_node"; - }else if(node->getType() == GameTreeNode::GameTreeNodeType::SHOWDOWN) { + write_comma(); + stream << "\"node_type\":\"showdown_node\""; }else if(node->getType() == GameTreeNode::GameTreeNodeType::TERMINAL) { + write_comma(); + stream << "\"node_type\":\"terminal_node\""; }else if(node->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { - json* retval; - if(key != ""){ - strategy[key] = json(); - retval = &(strategy[key]); - }else{ - retval = &strategy; - } - shared_ptr chanceNode = std::dynamic_pointer_cast(node); - const vector& cards = chanceNode->getCards(); - shared_ptr childerns = chanceNode->getChildren(); - vector card_strs; - for(Card card:cards) - card_strs.push_back(card.toString()); - - json& dealcards = (*retval)["dealcards"]; - for(std::size_t i = 0;i < cards.size();i ++){ - vector> new_exchange_color_list(exchange_color_list); - Card& one_card = const_cast(cards[i]); - vector new_prefix(prefix); - new_prefix.push_back("Chance:" + one_card.toString()); - - std::size_t card = i; - - int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; - if(offset < 0) { - for(std::size_t x = 0;x < cards.size();x ++){ - if( - Card::card2int(cards[x]) == - (Card::card2int(cards[card]) + offset) - ){ - card = x; - break; - } - } - if(card == i){ - throw runtime_error("isomorphism not found while dump strategy"); - } - vector one_exchange{one_card.getCardInt() % 4,one_card.getCardInt() % 4 + offset}; - new_exchange_color_list.push_back(one_exchange); + + write_comma(); + stream << "\"node_type\":\"chance_node\""; + + if (this->analysis_mode == Solver::AnalysisMode::HAND_ANALYSIS) { + GameTreeNode::GameRound round = chanceNode->getRound(); + Card next_card; + if (round == GameTreeNode::GameRound::TURN) { + if (full_board_cards.size() < 4) throw runtime_error("Hand analysis dump requires at least 4 board cards for flop->turn."); + next_card = full_board_cards[3]; + } else if (round == GameTreeNode::GameRound::RIVER) { + if (full_board_cards.size() < 5) throw runtime_error("Hand analysis dump requires 5 board cards for turn->river."); + next_card = full_board_cards[4]; + } else { + throw runtime_error("Hand analysis dump is only for post-flop chance nodes."); + } + + write_comma(); + stream << "\"dealcards\":{"; + + int card_idx = next_card.getNumberInDeckInt(); + if (card_idx < 0) { + throw runtime_error("Card from full_board has invalid deck index during dump: " + next_card.toString()); } - int card_num = this->deck.getCards().size(); int new_deal; - if(deal == 0){ - new_deal = card + 1; - } else if (deal > 0 && deal <= card_num){ + int card_num = this->deck.getCards().size(); + if (deal == 0) { + new_deal = card_idx + 1; + } else if (deal > 0 && deal <= card_num) { int origin_deal = deal - 1; - -#ifdef DEBUG - if(origin_deal == card) throw runtime_error("deal should not be equal"); -#endif - new_deal = card_num * origin_deal + card; + new_deal = card_num * origin_deal + card_idx; new_deal += (1 + card_num); - } else{ + } else { throw runtime_error(tfm::format("deal out of range : %s ",deal)); } - if(exchange_color_list.size() > 1){ - throw runtime_error("exchange color list shouldn't be exceed size 1 here"); - } + vector new_prefix(prefix); + new_prefix.push_back("Chance:" + next_card.toString()); + + this->reConvertJson(stream, chanceNode->getChildren(), next_card.toString(), depth + 1, max_depth, new_prefix, new_deal, exchange_color_list); - string one_card_str = one_card.toString(); - if(exchange_color_list.size() == 1) { - int rank1 = exchange_color_list[0][0]; - int rank2 = exchange_color_list[0][1]; - if(one_card.getCardInt() % 4 == rank1){ - one_card_str = Card::intCard2Str(one_card.getCardInt() - rank1 + rank2); - }else if(one_card.getCardInt() % 4 == rank2){ - one_card_str = Card::intCard2Str(one_card.getCardInt() - rank2 + rank1); + stream << "}"; + write_comma(); + stream << "\"deal_number\":1"; + } else { + const vector& cards = chanceNode->getCards(); + shared_ptr child_node = chanceNode->getChildren(); + + if (!cards.empty()) { + write_comma(); + stream << "\"dealcards\":{"; + bool first_child = true; + for(std::size_t i = 0;i < cards.size();i ++){ + vector> new_exchange_color_list(exchange_color_list); + const Card& one_card = cards[i]; + vector new_prefix(prefix); + new_prefix.push_back("Chance:" + one_card.toString()); + + std::size_t card = i; + + int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; + if(offset < 0) { + for(std::size_t x = 0;x < cards.size();x ++){ + if( + Card::card2int(cards[x]) == + (Card::card2int(one_card) + offset) + ){ + card = x; + break; + } + } + if(card == i){ + throw runtime_error("isomorphism not found while dump strategy"); + } + vector one_exchange{one_card.getCardInt() % 4,one_card.getCardInt() % 4 + offset}; + new_exchange_color_list.push_back(one_exchange); } - } + int card_num = this->deck.getCards().size(); + int new_deal; + if(deal == 0){ + new_deal = card + 1; + } else if (deal > 0 && deal <= card_num){ + int origin_deal = deal - 1; + +#ifdef DEBUG + if(origin_deal == static_cast(card)) throw runtime_error("deal should not be equal"); +#endif + new_deal = card_num * origin_deal + card; + new_deal += (1 + card_num); + } else{ + throw runtime_error(tfm::format("deal out of range : %s ",deal)); + } - this->reConvertJson(childerns,dealcards,one_card_str,depth + 1,max_depth,new_prefix,new_deal,new_exchange_color_list); + if(exchange_color_list.size() > 1){ + throw runtime_error("exchange color list shouldn't be exceed size 1 here"); + } + + string one_card_str = one_card.toString(); + if(exchange_color_list.size() == 1) { + int rank1 = exchange_color_list[0][0]; + int rank2 = exchange_color_list[0][1]; + if(one_card.getCardInt() % 4 == rank1){ + one_card_str = Card::intCard2Str(one_card.getCardInt() - rank1 + rank2); + }else if(one_card.getCardInt() % 4 == rank2){ + one_card_str = Card::intCard2Str(one_card.getCardInt() - rank2 + rank1); + } + } + + if (!first_child) stream << ","; + first_child = false; + this->reConvertJson(stream, child_node, one_card_str, depth + 1, max_depth, new_prefix, new_deal, new_exchange_color_list); + } + stream << "}"; + write_comma(); + stream << "\"deal_number\":" << cards.size(); + } else { + write_comma(); + stream << "\"deal_number\":0"; } - if((*retval)["dealcards"].empty()){ - (*retval).erase("dealcards"); } - - (*retval)["deal_number"] = dealcards.size(); - (*retval)["node_type"] = "chance_node"; }else{ throw runtime_error("node type unknown!!"); } + stream << "}"; } vector>> PCfrSolver::get_strategy(shared_ptr node,vector chance_cards){ @@ -1022,10 +1156,25 @@ vector>> PCfrSolver::get_strategy(shared_ptr no } } - vector& cards = this->deck.getCards(); + vector relevant_chance_cards; + if (node->getRound() != this->root_round) { + // Determine how many chance cards are needed to get from the root street to the node's street. + int root_round_int = GameTreeNode::gameRound2int(this->root_round); + int node_round_int = GameTreeNode::gameRound2int(node->getRound()); + int cards_to_take = node_round_int - root_round_int; - for(Card one_card: chance_cards){ + if (cards_to_take > 0 && chance_cards.size() >= static_cast(cards_to_take)) { + for (int i = 0; i < cards_to_take; ++i) { + relevant_chance_cards.push_back(chance_cards[i]); + } + } + } + + const vector& cards = this->deck.getCards(); + + for(Card one_card: relevant_chance_cards){ int card = one_card.getNumberInDeckInt(); + if (card < 0) throw runtime_error("Card in get_strategy has no deck index: " + one_card.toString()); int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; if(offset < 0) { for(std::size_t x = 0;x < cards.size();x ++){ @@ -1057,6 +1206,9 @@ vector>> PCfrSolver::get_strategy(shared_ptr no deal = new_deal; } shared_ptr trainable = node->getTrainable(deal,true,this->use_halffloats); + if (!trainable) { + return ret_strategy; + } json retjson = trainable->dump_strategy(false);; for(vector one_exchange:exchange_color_list){ @@ -1106,10 +1258,25 @@ vector>> PCfrSolver::get_evs(shared_ptr node,ve } } - vector& cards = this->deck.getCards(); + vector relevant_chance_cards; + if (node->getRound() != this->root_round) { + // Determine how many chance cards are needed to get from the root street to the node's street. + int root_round_int = GameTreeNode::gameRound2int(this->root_round); + int node_round_int = GameTreeNode::gameRound2int(node->getRound()); + int cards_to_take = node_round_int - root_round_int; + + if (cards_to_take > 0 && chance_cards.size() >= static_cast(cards_to_take)) { + for (int i = 0; i < cards_to_take; ++i) { + relevant_chance_cards.push_back(chance_cards[i]); + } + } + } + + const vector& cards = this->deck.getCards(); - for(Card one_card: chance_cards){ + for(Card one_card: relevant_chance_cards){ int card = one_card.getNumberInDeckInt(); + if (card < 0) throw runtime_error("Card in get_evs has no deck index: " + one_card.toString()); int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; if(offset < 0) { for(std::size_t x = 0;x < cards.size();x ++){ @@ -1141,6 +1308,9 @@ vector>> PCfrSolver::get_evs(shared_ptr node,ve deal = new_deal; } shared_ptr trainable = node->getTrainable(deal,true,this->use_halffloats); + if (!trainable) { + return ret_evs; + } json retjson = trainable->dump_evs(); for(vector one_exchange:exchange_color_list){ @@ -1176,12 +1346,18 @@ vector>> PCfrSolver::get_evs(shared_ptr node,ve return ret_evs; } +void PCfrSolver::dumps(std::ostream& stream, bool with_status, int depth) { + if (with_status) { + throw runtime_error("Streaming dump with status is not supported."); + } + reConvertJson(stream, this->tree->getRoot(), "", 0, depth, vector({"begin"}), 0, vector>()); +} + json PCfrSolver::dumps(bool with_status,int depth) { if(with_status == true){ throw runtime_error(""); } - json retjson; - this->reConvertJson(this->tree->getRoot(),retjson,"",0,depth,vector({"begin"}),0,vector>()); - return std::move(retjson); + std::stringstream ss; + dumps(ss, with_status, depth); + return json::parse(ss.str()); } - diff --git a/src/tools/CommandLineTool.cpp b/src/tools/CommandLineTool.cpp index 03e81080..57f9af5b 100644 --- a/src/tools/CommandLineTool.cpp +++ b/src/tools/CommandLineTool.cpp @@ -35,25 +35,6 @@ CommandLineTool::CommandLineTool(string mode,string resource_dir) { StreetSetting gbs_river_oop = StreetSetting(vector{},vector{},vector{},true); this->gtbs = make_shared(gbs_flop_ip,gbs_turn_ip,gbs_river_ip,gbs_flop_oop,gbs_turn_oop,gbs_river_oop); - //ps.build_game_tree(oop_commit,ip_commit,current_round,raise_limit,small_blind,big_blind,stack,*gtbs.get(),allin_threshold); - //cout << "build tree finished" << endl; - /* - ps.getGameTree()->recurrentPrintTree(ps.getGameTree()->getRoot(),0,4); - ps.train( - //"AA:0.001,KK:0.001,QQ:0.001,JJ:0.001,TT:0.001,99:0.001,88:0.001,77:0.001,66:0.001,55:0.001,44:0.24,33:0.2,22:0.5,AK:0.001,AQ:0.001,AJ:0.001,ATs:0.16,ATo:0.3,A9s,A9o:0.15,A8s:0.15,A8o:0.001,A7s:0.38,A7o:0.001,A6s:0.35,A6o:0.001,A5s:0.35,A5o:0.001,A4s:0.35,A4o:0.001,A3s:0.5,A3o:0.001,A2:0.001,KQs:0.05,KQo:0.001,KJs:0.4,KJo:0.5,KTs:0.6,KTo:0.2,K9s,K9o:0.001,K8s:0.55,K8o:0.001,K7:0.001,K6:0.001,K5:0.001,K4:0.001,K3:0.001,K2:0.001,QJs:0.001,QJo:0.58,QTs:0.1,QTo:0.08,Q9s:0.75,Q9o:0.001,Q8:0.001,Q7:0.001,Q6:0.001,Q5:0.001,Q4:0.001,Q3:0.001,Q2:0.001,JT:0.001,J9s:0.03,J9o:0.001,J8:0.001,J7:0.001,J6:0.001,J5:0.001,J4:0.001,J3:0.001,J2:0.001,T9s:0.06,T9o:0.001,T8s:0.55,T8o:0.001,T7:0.001,T6:0.001,T5:0.001,T4:0.001,T3:0.001,T2:0.001,98s:0.2,98o:0.001,97:0.001,96:0.001,95:0.001,94:0.001,93:0.001,92:0.001,87s:0.3,87o:0.001,86:0.001,85:0.001,84:0.001,83:0.001,82:0.001,76s:0.3,76o:0.001,75:0.001,74:0.001,73:0.001,72:0.001,65s:0.37,65o:0.001,64:0.001,63:0.001,62:0.001,54:0.001,53:0.001,52:0.001,43:0.001,42:0.001,32:0.001", - //"AA,KK,QQ,JJ,TT,99,88,77,66,55,44:0.925000012,33:0.95,22:0.200050003,AK,AQ,AJ,AT,A9,A8,A7,A6s,A6o:0.849999994,A5s,A5o:0.949999988,A4s,A4o:0.0500500005,A3s,A3o:0.0250500004,A2s,A2o:0.0250500004,KQ,KJ,KT,K9s:0.925000012,K9o,K8s,K8o:0.337499997,K7s,K7o:0.0500500005,K6s,K6o:0.00055000005,K5s,K5o:0.00055000005,K4s,K4o:0.00055000005,K3s,K3o:0.00055000005,K2s:0.200050003,K2o:0.00055000005,QJ,QT,Q9s,Q9o:0.949999988,Q8s,Q8o:0.0500500005,Q7s,Q7o:0.00055000005,Q6s,Q6o:0.00055000005,Q5s,Q5o:0.00055000005,Q4s:0.799999988,Q4o:0.00055000005,Q3s:0.0500500005,Q3o:0.00055000005,Q2s:0.0250500004,Q2o:0.00055000005,JT,J9:0.900000006,J8s,J8o:0.0500500005,J7s,J7o:0.00055000005,J6s,J6o:0.00055000005,J5s:0.524999988,J5o:0.00055000005,J4s:0.0500500005,J4o:0.00055000005,J3s:0.0250500004,J3o:0.00055000005,J2:0.00055000005,T9s,T9o:0.949999988,T8s,T8o:0.0250500004,T7s,T7o:0.00055000005,T6s,T6o:0.00055000005,T5s:0.0500500005,T5o:0.00055000005,T4s:0.0250500004,T4o:0.00055000005,T3:0.00055000005,T2:0.00055000005,98s,98o:0.300000002,97s,97o:0.00055000005,96s,96o:0.00055000005,95s:0.0500500005,95o:0.00055000005,94:0.00055000005,93:0.00055000005,92:0.00055000005,87s,87o:0.00055000005,86s,86o:0.00055000005,85s:0.949999988,85o:0.00055000005,84:0.00055000005,83:0.00055000005,82:0.00055000005,76s,76o:0.00055000005,75s,75o:0.00055000005,74s:0.0500500005,74o:0.00055000005,73:0.00055000005,72:0.00055000005,65s,65o:0.00055000005,64s:0.25005,64o:0.00055000005,63:0.00055000005,62:0.00055000005,54s:0.949999988,54o:0.00055000005,53s:0.0250500004,53o:0.00055000005,52:0.00055000005,43s:0.0500500005,43o:0.00055000005,42:0.00055000005,32:0.00055000005", - "AA,KK,QQ,JJ,TT,99:0.75,88:0.75,77:0.5,66:0.25,55:0.25,AK,AQs,AQo:0.75,AJs,AJo:0.5,ATs:0.75,A6s:0.25,A5s:0.75,A4s:0.75,A3s:0.5,A2s:0.5,KQs,KQo:0.5,KJs,KTs:0.75,K5s:0.25,K4s:0.25,QJs:0.75,QTs:0.75,Q9s:0.5,JTs:0.75,J9s:0.75,J8s:0.75,T9s:0.75,T8s:0.75,T7s:0.75,98s:0.75,97s:0.75,96s:0.5,87s:0.75,86s:0.5,85s:0.5,76s:0.75,75s:0.5,65s:0.75,64s:0.5,54s:0.75,53s:0.5,43s:0.5", - "QQ:0.5,JJ:0.75,TT,99,88,77,66,55,44,33,22,AKo:0.25,AQs,AQo:0.75,AJs,AJo:0.75,ATs,ATo:0.75,A9s,A8s,A7s,A6s,A5s,A4s,A3s,A2s,KQ,KJ,KTs,KTo:0.5,K9s,K8s,K7s,K6s,K5s,K4s:0.5,K3s:0.5,K2s:0.5,QJ,QTs,Q9s,Q8s,Q7s,JTs,JTo:0.5,J9s,J8s,T9s,T8s,T7s,98s,97s,96s,87s,86s,76s,75s,65s,64s,54s,53s,43s", - "Qs,Jh,2h", - logfile_name, - 300, - 10, - "discounted_cfr", - -1, - true, - 8 - ); - */ } void CommandLineTool::startWorking() { @@ -78,15 +59,12 @@ void split(const string& s, char c, vector& v) { string::size_type i = 0; string::size_type j = s.find(c); - while (j != string::npos) { - v.push_back(s.substr(i, j-i)); - i = ++j; - j = s.find(c, j); - - if (j == string::npos) - v.push_back(s.substr(i, s.length())); + v.push_back(s.substr(i, j - i)); + i = j + 1; + j = s.find(c, i); } + v.push_back(s.substr(i)); } @@ -154,8 +132,23 @@ void CommandLineTool::processCommand(string input) { this->use_isomorphism = stoi(paramstr); }else if(command == "set_print_interval"){ this->print_interval = stoi(paramstr); + }else if(command == "set_raise_limit"){ + this->raise_limit = stoi(paramstr); }else if(command == "start_solve"){ + if (this->hand_analysis) { + this->ps.build_game_tree(oop_commit,ip_commit, 1 /* FLOP */,raise_limit,small_blind,big_blind,stack,*gtbs.get(),allin_threshold); + } + if (this->ps.getGameTree() == nullptr) { + throw runtime_error("Game tree not built. Please use the build_tree command first."); + } + if (this->board.empty()) { + throw runtime_error("Board is empty. Please use the set_board command first."); + } cout << "<<>>" << endl; + this->ps.analysis_mode = this->hand_analysis ? Solver::AnalysisMode::HAND_ANALYSIS : Solver::AnalysisMode::STANDARD; + if (this->hand_analysis) { + this->ps.full_board = this->board; + } this->ps.train( this->range_ip, this->range_oop, @@ -175,6 +168,8 @@ void CommandLineTool::processCommand(string input) { this->ps.dump_strategy(QString::fromStdString(output_file),this->dump_rounds); }else if(command == "set_dump_rounds"){ this->dump_rounds = stoi(paramstr); + }else if(command == "set_hand_analysis"){ + this->hand_analysis = (stoi(paramstr) != 0); }else{ cout << "command not recognized: " << command << endl; } diff --git a/src/trainable/DiscountedCfrTrainable.cpp b/src/trainable/DiscountedCfrTrainable.cpp index 98f6faa3..18f12762 100644 --- a/src/trainable/DiscountedCfrTrainable.cpp +++ b/src/trainable/DiscountedCfrTrainable.cpp @@ -3,6 +3,8 @@ // #include "include/trainable/DiscountedCfrTrainable.h" +#include + //#define DEBUG; DiscountedCfrTrainable::DiscountedCfrTrainable(vector *privateCards, @@ -16,7 +18,6 @@ DiscountedCfrTrainable::DiscountedCfrTrainable(vector *privateCard this->r_plus_sum = vector(this->card_number,0.0); this->cum_r_plus = vector(this->action_number * this->card_number,0.0); - //this->cum_r_plus_sum = vector(this->card_number); } bool DiscountedCfrTrainable::isAllZeros(const vector& input_array) { @@ -95,15 +96,12 @@ void DiscountedCfrTrainable::updateRegrets(const vector& regrets, int ite auto alpha_coef = pow(iteration_number, this->alpha); alpha_coef = alpha_coef / (1 + alpha_coef); - //Arrays.fill(this.r_plus_sum,0); fill(r_plus_sum.begin(),r_plus_sum.end(),0); - //fill(cum_r_plus_sum.begin(),cum_r_plus_sum.end(),0); for (int action_id = 0;action_id < action_number;action_id ++) { for(int private_id = 0;private_id < this->card_number;private_id ++){ int index = action_id * this->card_number + private_id; float one_reg = regrets[index]; - // 更新 R+ this->r_plus[index] = one_reg + this->r_plus[index]; if(this->r_plus[index] > 0){ this->r_plus[index] *= alpha_coef; @@ -112,10 +110,6 @@ void DiscountedCfrTrainable::updateRegrets(const vector& regrets, int ite } this->r_plus_sum[private_id] += max(float(0.0),this->r_plus[index]); - - // 更新累计策略 - // this.cum_r_plus[index] += this.r_plus[index] * iteration_number; - // this.cum_r_plus_sum[private_id] += this.cum_r_plus[index]; } } vector current_strategy = this->getcurrentStrategyNoCache(); @@ -124,40 +118,83 @@ void DiscountedCfrTrainable::updateRegrets(const vector& regrets, int ite for(int private_id = 0;private_id < this->card_number;private_id ++) { int index = action_id * this->card_number + private_id; this->cum_r_plus[index] *= this->theta; - this->cum_r_plus[index] += current_strategy[index] * strategy_coef;// * reach_probs[private_id]; - //this->cum_r_plus_sum[private_id] += this->cum_r_plus[index] ; + this->cum_r_plus[index] += current_strategy[index] * strategy_coef; } } } -json DiscountedCfrTrainable::dump_strategy(bool with_state) { - if(with_state) throw runtime_error("state storage not implemented"); +void DiscountedCfrTrainable::dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) { + if (with_state) { + stream << "{}"; + return; + } - json strategy; - const vector& average_strategy = this->getAverageStrategy(); - vector& game_actions = action_node.getActions(); - vector actions_str; - for(GameActions& one_action:game_actions) { - actions_str.push_back( - one_action.toString() - ); + vector transformed_strategy = this->getAverageStrategy(); + + if (!exchange_color_list.empty()) { + const vector& range = *this->privateCards; + unordered_map hand_hash_to_index; + for(size_t i = 0; i < range.size(); ++i) { + hand_hash_to_index[range[i].hashCode()] = i; + } + + for (const auto& one_exchange : exchange_color_list) { + int rank1 = one_exchange[0]; + int rank2 = one_exchange[1]; + if (rank1 == rank2) continue; + + vector swapped(range.size(), false); + for(std::size_t i = 0; i < range.size(); i++){ + if (swapped[i]) continue; + + const PrivateCards& pc_i = range[i]; + PrivateCards pc_j = pc_i.exchange_color(rank1, rank2); + auto it = hand_hash_to_index.find(pc_j.hashCode()); + + if (it != hand_hash_to_index.end()) { + size_t j = it->second; + if (i < j) { + for (int action_id = 0; action_id < this->action_number; ++action_id) { + size_t index_i = action_id * this->card_number + i; + size_t index_j = action_id * this->card_number + j; + std::swap(transformed_strategy[index_i], transformed_strategy[index_j]); + } + swapped[i] = true; + swapped[j] = true; + } + } + } + } } - for(std::size_t i = 0;i < this->privateCards->size();i ++){ - PrivateCards& one_private_card = (*this->privateCards)[i]; - vector one_strategy(this->action_number); + stream << "{\"actions\":["; + const auto& game_actions = action_node.getActions(); + for (size_t i = 0; i < game_actions.size(); ++i) { + stream << "\"" << game_actions[i].toString() << "\""; + if (i < game_actions.size() - 1) stream << ","; + } + stream << "],\"strategy\":{"; - for(int j = 0;j < this->action_number;j ++){ - std::size_t strategy_index = j * this->privateCards->size() + i; - one_strategy[j] = average_strategy[strategy_index]; + for(std::size_t i = 0; i < this->privateCards->size(); i++) { + const PrivateCards& one_private_card = (*this->privateCards)[i]; + stream << "\"" << one_private_card.toString() << "\":["; + + for(int j = 0; j < this->action_number; j++) { + std::size_t strategy_index = j * this->card_number + i; + stream << transformed_strategy[strategy_index]; + if (j < this->action_number - 1) stream << ","; } - strategy[tfm::format("%s",one_private_card.toString())] = one_strategy; + stream << "]"; + if (i < this->privateCards->size() - 1) stream << ","; } + stream << "}}"; +} - json retjson; - retjson["actions"] = std::move(actions_str); - retjson["strategy"] = std::move(strategy); - return std::move(retjson); + +json DiscountedCfrTrainable::dump_strategy(bool with_state) { + std::stringstream ss; + dump_strategy(ss, with_state, {}, std::make_shared(action_node)); + return json::parse(ss.str()); } json DiscountedCfrTrainable::dump_evs() { diff --git a/src/trainable/DiscountedCfrTrainableHF.cpp b/src/trainable/DiscountedCfrTrainableHF.cpp index f9aa4aae..fbb3b881 100644 --- a/src/trainable/DiscountedCfrTrainableHF.cpp +++ b/src/trainable/DiscountedCfrTrainableHF.cpp @@ -2,6 +2,8 @@ // based DiscountableCfrTrainable.h from Xuefeng Huang on 2020/1/31. #include "include/trainable/DiscountedCfrTrainableHF.h" +#include + //#define DEBUG; DiscountedCfrTrainableHF::DiscountedCfrTrainableHF(vector *privateCards, @@ -152,34 +154,77 @@ void DiscountedCfrTrainableHF::updateRegrets(const vector& regrets, int i } } -json DiscountedCfrTrainableHF::dump_strategy(bool with_state) { - if(with_state) throw runtime_error("state storage not implemented"); +void DiscountedCfrTrainableHF::dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) { + if (with_state) { + stream << "{}"; + return; + } - json strategy; - const vector& average_strategy = this->getAverageStrategy(); - vector& game_actions = action_node.getActions(); - vector actions_str; - for(GameActions& one_action:game_actions) { - actions_str.push_back( - one_action.toString() - ); + vector transformed_strategy = this->getAverageStrategy(); + + if (!exchange_color_list.empty()) { + const vector& range = *this->privateCards; + unordered_map hand_hash_to_index; + for(size_t i = 0; i < range.size(); ++i) { + hand_hash_to_index[range[i].hashCode()] = i; + } + + for (const auto& one_exchange : exchange_color_list) { + int rank1 = one_exchange[0]; + int rank2 = one_exchange[1]; + if (rank1 == rank2) continue; + + vector swapped(range.size(), false); + for(std::size_t i = 0; i < range.size(); i++){ + if (swapped[i]) continue; + + const PrivateCards& pc_i = range[i]; + PrivateCards pc_j = pc_i.exchange_color(rank1, rank2); + auto it = hand_hash_to_index.find(pc_j.hashCode()); + + if (it != hand_hash_to_index.end()) { + size_t j = it->second; + if (i < j) { + for (int action_id = 0; action_id < this->action_number; ++action_id) { + size_t index_i = action_id * this->card_number + i; + size_t index_j = action_id * this->card_number + j; + std::swap(transformed_strategy[index_i], transformed_strategy[index_j]); + } + swapped[i] = true; + swapped[j] = true; + } + } + } + } } - for(std::size_t i = 0;i < this->privateCards->size();i ++){ - PrivateCards& one_private_card = (*this->privateCards)[i]; - vector one_strategy(this->action_number); + stream << "{\"actions\":["; + const auto& game_actions = node->getActions(); + for (size_t i = 0; i < game_actions.size(); ++i) { + stream << "\"" << game_actions[i].toString() << "\""; + if (i < game_actions.size() - 1) stream << ","; + } + stream << "],\"strategy\":{"; - for(int j = 0;j < this->action_number;j ++){ - int strategy_index = j * this->privateCards->size() + i; - one_strategy[j] = average_strategy[strategy_index]; + for(std::size_t i = 0; i < this->privateCards->size(); i++) { + const PrivateCards& one_private_card = (*this->privateCards)[i]; + stream << "\"" << one_private_card.toString() << "\":["; + + for(int j = 0; j < this->action_number; j++) { + std::size_t strategy_index = j * this->card_number + i; + stream << transformed_strategy[strategy_index]; + if (j < this->action_number - 1) stream << ","; } - strategy[tfm::format("%s",one_private_card.toString())] = one_strategy; + stream << "]"; + if (i < this->privateCards->size() - 1) stream << ","; } + stream << "}}"; +} - json retjson; - retjson["actions"] = std::move(actions_str); - retjson["strategy"] = std::move(strategy); - return std::move(retjson); +json DiscountedCfrTrainableHF::dump_strategy(bool with_state) { + std::stringstream ss; + dump_strategy(ss, with_state, {}, std::make_shared(action_node)); + return json::parse(ss.str()); } json DiscountedCfrTrainableHF::dump_evs() { diff --git a/src/trainable/DiscountedCfrTrainableSF.cpp b/src/trainable/DiscountedCfrTrainableSF.cpp index b1650960..a3c56aae 100644 --- a/src/trainable/DiscountedCfrTrainableSF.cpp +++ b/src/trainable/DiscountedCfrTrainableSF.cpp @@ -2,6 +2,8 @@ // based DiscountableCfrTrainable.h from Xuefeng Huang on 2020/1/31. #include "include/trainable/DiscountedCfrTrainableSF.h" +#include + //#define DEBUG; DiscountedCfrTrainableSF::DiscountedCfrTrainableSF(vector *privateCards, @@ -144,34 +146,77 @@ void DiscountedCfrTrainableSF::updateRegrets(const vector& regrets, int i } } -json DiscountedCfrTrainableSF::dump_strategy(bool with_state) { - if(with_state) throw runtime_error("state storage not implemented"); +void DiscountedCfrTrainableSF::dump_strategy(std::ostream& stream, bool with_state, const vector>& exchange_color_list, const shared_ptr& node) { + if (with_state) { + stream << "{}"; + return; + } - json strategy; - const vector& average_strategy = this->getAverageStrategy(); - vector& game_actions = action_node.getActions(); - vector actions_str; - for(GameActions& one_action:game_actions) { - actions_str.push_back( - one_action.toString() - ); + vector transformed_strategy = this->getAverageStrategy(); + + if (!exchange_color_list.empty()) { + const vector& range = *this->privateCards; + unordered_map hand_hash_to_index; + for(size_t i = 0; i < range.size(); ++i) { + hand_hash_to_index[range[i].hashCode()] = i; + } + + for (const auto& one_exchange : exchange_color_list) { + int rank1 = one_exchange[0]; + int rank2 = one_exchange[1]; + if (rank1 == rank2) continue; + + vector swapped(range.size(), false); + for(std::size_t i = 0; i < range.size(); i++){ + if (swapped[i]) continue; + + const PrivateCards& pc_i = range[i]; + PrivateCards pc_j = pc_i.exchange_color(rank1, rank2); + auto it = hand_hash_to_index.find(pc_j.hashCode()); + + if (it != hand_hash_to_index.end()) { + size_t j = it->second; + if (i < j) { + for (int action_id = 0; action_id < this->action_number; ++action_id) { + size_t index_i = action_id * this->card_number + i; + size_t index_j = action_id * this->card_number + j; + std::swap(transformed_strategy[index_i], transformed_strategy[index_j]); + } + swapped[i] = true; + swapped[j] = true; + } + } + } + } } - for(std::size_t i = 0;i < this->privateCards->size();i ++){ - PrivateCards& one_private_card = (*this->privateCards)[i]; - vector one_strategy(this->action_number); + stream << "{\"actions\":["; + const auto& game_actions = node->getActions(); + for (size_t i = 0; i < game_actions.size(); ++i) { + stream << "\"" << game_actions[i].toString() << "\""; + if (i < game_actions.size() - 1) stream << ","; + } + stream << "],\"strategy\":{"; - for(int j = 0;j < this->action_number;j ++){ - std::size_t strategy_index = j * this->privateCards->size() + i; - one_strategy[j] = average_strategy[strategy_index]; + for(std::size_t i = 0; i < this->privateCards->size(); i++) { + const PrivateCards& one_private_card = (*this->privateCards)[i]; + stream << "\"" << one_private_card.toString() << "\":["; + + for(int j = 0; j < this->action_number; j++) { + std::size_t strategy_index = j * this->card_number + i; + stream << transformed_strategy[strategy_index]; + if (j < this->action_number - 1) stream << ","; } - strategy[tfm::format("%s",one_private_card.toString())] = one_strategy; + stream << "]"; + if (i < this->privateCards->size() - 1) stream << ","; } + stream << "}}"; +} - json retjson; - retjson["actions"] = std::move(actions_str); - retjson["strategy"] = std::move(strategy); - return std::move(retjson); +json DiscountedCfrTrainableSF::dump_strategy(bool with_state) { + std::stringstream ss; + dump_strategy(ss, with_state, {}, std::make_shared(action_node)); + return json::parse(ss.str()); } json DiscountedCfrTrainableSF::dump_evs() { diff --git a/src/ui/detailitemdelegate.cpp b/src/ui/detailitemdelegate.cpp index 4d6040b2..177dba8e 100644 --- a/src/ui/detailitemdelegate.cpp +++ b/src/ui/detailitemdelegate.cpp @@ -5,32 +5,31 @@ #include #include -DetailItemDelegate::DetailItemDelegate(DetailWindowSetting* detailWindowSetting,QObject *parent) : - WordItemDelegate(parent) -{ - this->detailWindowSetting = detailWindowSetting; -} +DetailItemDelegate::DetailItemDelegate(QObject *parent) : + WordItemDelegate(parent) {} void DetailItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto options = option; initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); - //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); - options.text = ""; - if(detailViewerModel->tableStrategyModel->treeItem != NULL && - detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock()->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); + options.text = ""; + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + + shared_ptr node = detailViewerModel->tableStrategyModel->getCurrentNode().lock(); + if(node && node->getType() == GameTreeNode::GameTreeNode::ACTION){ + const DetailWindowSetting* dws = detailViewerModel->tableStrategyModel->get_detail_window_setting(); + if (!dws) return; int strategy_number = 0; - if(this->detailWindowSetting->grid_i >= 0 && this->detailWindowSetting->grid_j >= 0){ - strategy_number = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size(); + if(dws->grid_i >= 0 && dws->grid_j >= 0 && !detailViewerModel->tableStrategyModel->ui_strategy_table.empty()){ + strategy_number = detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j].size(); } int ind = index.row() * detailViewerModel->columns + index.column(); if(ind < strategy_number){ - pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j][ind]; + pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j][ind]; int card1 = strategy_ui_table.first; int card2 = strategy_ui_table.second; vector strategy = detailViewerModel->tableStrategyModel->current_strategy[card1][card2]; @@ -116,24 +115,24 @@ void DetailItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionVie options.text = ""; options.text += detailViewerModel->tableStrategyModel->cardint2card[card1].toFormattedHtml(); options.text += detailViewerModel->tableStrategyModel->cardint2card[card2].toFormattedHtml(); - options.text = "

" + options.text + "<\/h2>"; + options.text = "

" + options.text + "

"; for(std::size_t i = 0;i < strategy.size();i ++){ GameActions one_action = gameActions[i]; float one_strategy = strategy[i] * 100; if(one_action.getAction() == GameTreeNode::PokerActions::FOLD){ - options.text += QString("
%1 : %2\%<\/h5>").arg(tr("FOLD"),QString::number(one_strategy,'f',1)); + options.text += QString("
%1 : %2%
").arg(tr("FOLD"),QString::number(one_strategy,'f',1)); } else if(one_action.getAction() == GameTreeNode::PokerActions::CALL){ - options.text += QString("
%1 : %2\%<\/h5>").arg(tr("CALL"),QString::number(one_strategy,'f',1)); + options.text += QString("
%1 : %2%
").arg(tr("CALL"),QString::number(one_strategy,'f',1)); } else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK){ - options.text += QString("
%1 : %2\%<\/h5>").arg(tr("CHECK"),QString::number(one_strategy,'f',1)); + options.text += QString("
%1 : %2%
").arg(tr("CHECK"),QString::number(one_strategy,'f',1)); } else if(one_action.getAction() == GameTreeNode::PokerActions::BET){ - options.text += QString("
%1 %2 : %3\%<\/h5>").arg(tr("BET"),QString::number(one_action.getAmount()),QString::number(one_strategy,'f',1)); + options.text += QString("
%1 %2 : %3%
").arg(tr("BET"),QString::number(one_action.getAmount()),QString::number(one_strategy,'f',1)); } else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE){ - options.text += QString("
%1 %2 : %3\%<\/h5>").arg(tr("RAISE"),QString::number(one_action.getAmount()),QString::number(one_strategy,'f',1)); + options.text += QString("
%1 %2 : %3%
").arg(tr("RAISE"),QString::number(one_action.getAmount()),QString::number(one_strategy,'f',1)); } } } @@ -155,22 +154,25 @@ void DetailItemDelegate::paint_range(QPainter *painter, const QStyleOptionViewIt initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); - //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); options.text = ""; - if(detailViewerModel->tableStrategyModel->treeItem != NULL){ + if(detailViewerModel && detailViewerModel->tableStrategyModel && detailViewerModel->tableStrategyModel->getCurrentNode().lock()){ + const DetailWindowSetting* dws = detailViewerModel->tableStrategyModel->get_detail_window_setting(); + if (!dws) return; + vector> card_cords; - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ - card_cords = detailViewerModel->tableStrategyModel->ui_p1_range[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j]; + if(dws->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ + card_cords = detailViewerModel->tableStrategyModel->ui_p1_range[dws->grid_i][dws->grid_j]; }else{ - card_cords = detailViewerModel->tableStrategyModel->ui_p2_range[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j]; + card_cords = detailViewerModel->tableStrategyModel->ui_p2_range[dws->grid_i][dws->grid_j]; } - int ind = index.row() * detailViewerModel->columns + index.column(); + size_t ind = static_cast(index.row() * detailViewerModel->columns + index.column()); if(ind < card_cords.size()){ pair cord = card_cords[ind]; float range_number; - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ + + if(dws->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ range_number = detailViewerModel->tableStrategyModel->p1_range[cord.first][cord.second]; }else{ range_number = detailViewerModel->tableStrategyModel->p2_range[cord.first][cord.second]; @@ -191,9 +193,9 @@ void DetailItemDelegate::paint_range(QPainter *painter, const QStyleOptionViewIt options.text = ""; options.text += detailViewerModel->tableStrategyModel->cardint2card[cord.first].toFormattedHtml(); options.text += detailViewerModel->tableStrategyModel->cardint2card[cord.second].toFormattedHtml(); - options.text = "

" + options.text + "<\/h2>"; + options.text = "

" + options.text + "

"; - options.text += QString("

%1<\/h2>").arg(QString::number(range_number,'f',3)); + options.text += QString("

%1

").arg(QString::number(range_number,'f',3)); } } @@ -212,18 +214,21 @@ void DetailItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewItem const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); options.text = ""; - if(detailViewerModel->tableStrategyModel->treeItem != NULL && - detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock()->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + + shared_ptr node = detailViewerModel->tableStrategyModel->getCurrentNode().lock(); + if(node && node->getType() == GameTreeNode::GameTreeNode::ACTION){ int strategy_number = 0; - if(this->detailWindowSetting->grid_i >= 0 && this->detailWindowSetting->grid_j >= 0){ - strategy_number = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size(); + const DetailWindowSetting* dws = detailViewerModel->tableStrategyModel->get_detail_window_setting(); + if (!dws) return; + if(dws->grid_i >= 0 && dws->grid_j >= 0){ + strategy_number = static_cast(detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j].size()); } int ind = index.row() * detailViewerModel->columns + index.column(); if(ind < strategy_number){ - pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j][ind]; + pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j][ind]; int card1 = strategy_ui_table.first; int card2 = strategy_ui_table.second; shared_ptr actionNode = dynamic_pointer_cast(node); @@ -320,25 +325,25 @@ void DetailItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewItem options.text = ""; options.text += detailViewerModel->tableStrategyModel->cardint2card[card1].toFormattedHtml(); options.text += detailViewerModel->tableStrategyModel->cardint2card[card2].toFormattedHtml(); - options.text = "

" + options.text + "<\/h2>"; + options.text = "

" + options.text + "

"; for(std::size_t i = 0;i < evs.size();i ++){ GameActions one_action = gameActions[i]; QString one_ev = evs[i] != evs[i]? tr("Can't calculate"):QString::number(evs[i],'f',1); QString ev_str = tr("EV"); if(one_action.getAction() == GameTreeNode::PokerActions::FOLD){ - options.text += QString("
%1 %2: %3<\/h5>").arg(tr("FOLD"),ev_str,one_ev); + options.text += QString("
%1 %2: %3
").arg(tr("FOLD"),ev_str,one_ev); } else if(one_action.getAction() == GameTreeNode::PokerActions::CALL){ - options.text += QString("
%1 %2: %3<\/h5>").arg(tr("CALL"),ev_str,one_ev); + options.text += QString("
%1 %2: %3
").arg(tr("CALL"),ev_str,one_ev); } else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK){ - options.text += QString("
%1 %2: %3<\/h5>").arg(tr("CHECK"),ev_str,one_ev); + options.text += QString("
%1 %2: %3
").arg(tr("CHECK"),ev_str,one_ev); } else if(one_action.getAction() == GameTreeNode::PokerActions::BET){ - options.text += QString("
%1 %2 %3: %4<\/h5>").arg(tr("BET"),QString::number(one_action.getAmount()),ev_str,one_ev); + options.text += QString("
%1 %2 %3: %4
").arg(tr("BET"),QString::number(one_action.getAmount()),ev_str,one_ev); } else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE){ - options.text += QString("
%1 %2 %3: %4<\/h5>").arg(tr("RAISE"),QString::number(one_action.getAmount()),ev_str,one_ev); + options.text += QString("
%1 %2 %3: %4
").arg(tr("RAISE"),QString::number(one_action.getAmount()),ev_str,one_ev); } } } @@ -360,28 +365,28 @@ void DetailItemDelegate::paint_evs_only(QPainter *painter, const QStyleOptionVie initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); - //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); options.text = ""; - if(detailViewerModel->tableStrategyModel->treeItem != NULL && - detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock()->getType() == GameTreeNode::GameTreeNode::ACTION){ + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; - shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); + shared_ptr node = detailViewerModel->tableStrategyModel->getCurrentNode().lock(); + if(node && node->getType() == GameTreeNode::GameTreeNode::ACTION){ + const DetailWindowSetting* dws = detailViewerModel->tableStrategyModel->get_detail_window_setting(); + if (!dws) return; int strategy_number = 0; - if(this->detailWindowSetting->grid_i >= 0 && this->detailWindowSetting->grid_j >= 0){ - strategy_number = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size(); + if(dws->grid_i >= 0 && dws->grid_j >= 0){ + strategy_number = static_cast(detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j].size()); } - vector evs = detailViewerModel->tableStrategyModel->get_ev_grid(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); + vector evs = detailViewerModel->tableStrategyModel->get_ev_grid(dws->grid_i,dws->grid_j); std::size_t ind = index.row() * detailViewerModel->columns + index.column(); - if(ind < evs.size() and ind < strategy_number) + if(ind < evs.size() && ind < static_cast(strategy_number)) { float one_ev = evs[ind]; - float normalized_ev = normalization_tanh(detailViewerModel->tableStrategyModel->get_solver()->stack,one_ev); - //options.text += QString("
%1").arg(QString::number(normalized_ev)); + float normalized_ev = normalization_tanh(detailViewerModel->tableStrategyModel->get_qsolverjob()->stack,one_ev); - pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j][ind]; + pair strategy_ui_table = detailViewerModel->tableStrategyModel->ui_strategy_table[dws->grid_i][dws->grid_j][ind]; int card1 = strategy_ui_table.first; int card2 = strategy_ui_table.second; @@ -398,9 +403,9 @@ void DetailItemDelegate::paint_evs_only(QPainter *painter, const QStyleOptionVie options.text = ""; options.text += detailViewerModel->tableStrategyModel->cardint2card[card1].toFormattedHtml(); options.text += detailViewerModel->tableStrategyModel->cardint2card[card2].toFormattedHtml(); - options.text = "

" + options.text + "<\/h2>"; + options.text = "

" + options.text + "

"; - options.text += QString("

%1<\/h2>").arg(QString::number(one_ev,'f',3)); + options.text += QString("

%1

").arg(QString::number(one_ev,'f',3)); } } @@ -419,18 +424,25 @@ void DetailItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &op option.rect.width(), option.rect.height()); QBrush brush(Qt::gray); painter->fillRect(rect, brush); + + const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel || !detailViewerModel->tableStrategyModel->get_detail_window_setting()) { + painter->restore(); + return; + } + const DetailWindowSetting* dws = detailViewerModel->tableStrategyModel->get_detail_window_setting(); - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::STRATEGY){ + if(dws->mode == DetailWindowSetting::DetailWindowMode::STRATEGY){ this->paint_strategy(painter,option,index); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP || - this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_OOP ){ + else if(dws->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP || + dws->mode == DetailWindowSetting::DetailWindowMode::RANGE_OOP ){ this->paint_range(painter,option,index); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::EV){ + else if(dws->mode == DetailWindowSetting::DetailWindowMode::EV){ this->paint_evs(painter,option,index); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::EV_ONLY){ + else if(dws->mode == DetailWindowSetting::DetailWindowMode::EV_ONLY){ this->paint_evs_only(painter,option,index); } diff --git a/src/ui/rangeselectortabledelegate.cpp b/src/ui/rangeselectortabledelegate.cpp index f7e1f024..55d1c51f 100644 --- a/src/ui/rangeselectortabledelegate.cpp +++ b/src/ui/rangeselectortabledelegate.cpp @@ -1,40 +1,46 @@ #include "include/ui/rangeselectortabledelegate.h" +#include RangeSelectorTableDelegate::RangeSelectorTableDelegate(QStringList ranks,RangeSelectorTableModel *rangeSelectorTableModel,QObject *parent):WordItemDelegate(parent){ this->rank_list = ranks; this->rangeSelectorTableModel = rangeSelectorTableModel; } -void RangeSelectorTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const{ +void RangeSelectorTableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); - auto options = option; - initStyleOption(&options, index); - - QRect rect(option.rect.left(), option.rect.top(),\ - option.rect.width(), option.rect.height()); - QBrush brush(Qt::gray); - if(index.column() == index.row())brush = QBrush(Qt::darkGray); - painter->fillRect(rect, brush); - - float range_float = this->rangeSelectorTableModel->getRangeAt(index.row(),index.column()); - - float fold_prob = 1 - range_float; - int disable_height = (int)(fold_prob * option.rect.height()); - int remain_height = option.rect.height() - disable_height; - - rect = QRect(option.rect.left(), option.rect.top() + disable_height,\ - option.rect.width(), remain_height); - brush = QBrush(Qt::yellow); - painter->fillRect(rect, brush); - - QTextDocument doc; - doc.setHtml(options.text); - - painter->translate(options.rect.left(), options.rect.top()); - QRect clip(0, 0, options.rect.width(), options.rect.height()); - if(!this->rangeSelectorTableModel->in_thumbnail_mode()){ - doc.drawContents(painter, clip); + // Using a try-catch block is a robust way to ensure painter->restore() is always called, + // even if an exception is thrown during painting (e.g., from the model). + try { + auto options = option; + initStyleOption(&options, index); + + QRect rect(option.rect.left(), option.rect.top(), + option.rect.width(), option.rect.height()); + QBrush brush(Qt::gray); + if(index.column() == index.row())brush = QBrush(Qt::darkGray); + painter->fillRect(rect, brush); + + float range_float = this->rangeSelectorTableModel->getRangeAt(index.row(),index.column()); + + float fold_prob = 1.0f - range_float; + int disable_height = static_cast(fold_prob * static_cast(option.rect.height())); + int remain_height = option.rect.height() - disable_height; + + rect = QRect(option.rect.left(), option.rect.top() + disable_height, + option.rect.width(), remain_height); + brush = QBrush(Qt::yellow); + painter->fillRect(rect, brush); + + if(!this->rangeSelectorTableModel->in_thumbnail_mode()){ + // By default, the text is black. On a dark gray background, this is hard to see. + // We set the text color to the application's default text color for better visibility. + painter->setPen(QApplication::style()->standardPalette().color(QPalette::WindowText)); + painter->drawText(option.rect, Qt::AlignCenter, options.text); + } + } catch (...) { + painter->restore(); + throw; } painter->restore(); } diff --git a/src/ui/roughstrategyitemdelegate.cpp b/src/ui/roughstrategyitemdelegate.cpp index 5f24aa7a..6cd7b40c 100644 --- a/src/ui/roughstrategyitemdelegate.cpp +++ b/src/ui/roughstrategyitemdelegate.cpp @@ -5,11 +5,8 @@ #include #include -RoughStrategyItemDelegate::RoughStrategyItemDelegate(DetailWindowSetting* detailWindowSetting,QObject *parent) : - WordItemDelegate(parent) -{ - this->detailWindowSetting = detailWindowSetting; -} +RoughStrategyItemDelegate::RoughStrategyItemDelegate(QObject *parent) : + WordItemDelegate(parent) {} void RoughStrategyItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { auto options = option; @@ -18,9 +15,10 @@ void RoughStrategyItemDelegate::paint_strategy(QPainter *painter, const QStyleOp const RoughStrategyViewerModel * roughStrategyViewerModel = qobject_cast(index.model()); options.text = ""; - if(roughStrategyViewerModel->tableStrategyModel->treeItem != NULL && - roughStrategyViewerModel->tableStrategyModel->treeItem->m_treedata.lock()->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr node = roughStrategyViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); + if (!roughStrategyViewerModel || !roughStrategyViewerModel->tableStrategyModel) return; + + shared_ptr node = roughStrategyViewerModel->tableStrategyModel->getCurrentNode().lock(); + if(node && node->getType() == GameTreeNode::GameTreeNode::ACTION){ if(index.column() >= roughStrategyViewerModel->tableStrategyModel->total_strategy.size()) return; pair> one_strategy = roughStrategyViewerModel->tableStrategyModel->total_strategy[index.column()]; diff --git a/src/ui/roughstrategyviewermodel.cpp b/src/ui/roughstrategyviewermodel.cpp index 484a1bfa..1e270f8f 100644 --- a/src/ui/roughstrategyviewermodel.cpp +++ b/src/ui/roughstrategyviewermodel.cpp @@ -30,7 +30,7 @@ void RoughStrategyViewerModel::onchanged(){ int RoughStrategyViewerModel::columnCount(const QModelIndex &parent) const { - return this->tableStrategyModel->total_strategy.size(); + return static_cast(this->tableStrategyModel->total_strategy.size()); } int RoughStrategyViewerModel::rowCount(const QModelIndex &parent) const diff --git a/src/ui/strategyitemdelegate.cpp b/src/ui/strategyitemdelegate.cpp index b2adb6b1..60f4ff77 100644 --- a/src/ui/strategyitemdelegate.cpp +++ b/src/ui/strategyitemdelegate.cpp @@ -1,122 +1,123 @@ #include "include/ui/strategyitemdelegate.h" +#include "include/ui/tablestrategymodel.h" #include #include #include #include #include -StrategyItemDelegate::StrategyItemDelegate(QSolverJob * qSolverJob,DetailWindowSetting* detailWindowSetting,QObject *parent) : - WordItemDelegate(parent) -{ - this->detailWindowSetting = detailWindowSetting; - this->qSolverJob = qSolverJob; -} +StrategyItemDelegate::StrategyItemDelegate(QObject *parent) : + WordItemDelegate(parent) {} void StrategyItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, bool withEVs) const { auto options = option; initStyleOption(&options, index); const TableStrategyModel * tableStrategyModel = qobject_cast(index.model()); - if(tableStrategyModel->treeItem != NULL && - tableStrategyModel->treeItem->m_treedata.lock()->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr node = tableStrategyModel->treeItem->m_treedata.lock(); + + if (!tableStrategyModel) { + return; + } + + shared_ptr node = tableStrategyModel->getCurrentNode().lock(); + if (node && node->getType() == GameTreeNode::GameTreeNode::ACTION) { vector> strategy = tableStrategyModel->get_strategy(index.row(),index.column()); - vector evs = tableStrategyModel->get_strategies_evs(index.row(),index.column()); - if(!strategy.empty()){ - float fold_prob = 0; - vector strategy_without_fold; - vector evs_without_fold; - float strategy_without_fold_sum = 0; - for(std::size_t i = 0;i < strategy.size();i ++){ - GameActions one_action = strategy[i].first; - if(one_action.getAction() == GameTreeNode::PokerActions::FOLD){ - fold_prob = strategy[i].second; - }else{ - strategy_without_fold.push_back(strategy[i].second); - strategy_without_fold_sum += strategy[i].second; - evs_without_fold.push_back(evs[i]); + vector evs = tableStrategyModel->get_strategies_evs(index.row(),index.column()); + if(!strategy.empty()){ + float fold_prob = 0; + vector strategy_without_fold; + vector evs_without_fold; + float strategy_without_fold_sum = 0; + for(std::size_t i = 0;i < strategy.size();i ++){ + GameActions one_action = strategy[i].first; + if(one_action.getAction() == GameTreeNode::PokerActions::FOLD){ + fold_prob = strategy[i].second; + }else{ + strategy_without_fold.push_back(strategy[i].second); + strategy_without_fold_sum += strategy[i].second; + evs_without_fold.push_back(evs[i]); + } } - } - if(strategy_without_fold_sum > 0.){ - for(std::size_t i = 0;i < strategy_without_fold.size();i ++){ - strategy_without_fold[i] = strategy_without_fold[i] / strategy_without_fold_sum; + if(strategy_without_fold_sum > 0.){ + for(std::size_t i = 0;i < strategy_without_fold.size();i ++){ + strategy_without_fold[i] = strategy_without_fold[i] / strategy_without_fold_sum; + } } - } - // get range data - vector> card_cords; - card_cords = tableStrategyModel->ui_strategy_table[index.row()][index.column()]; - float range_number = 0; - if(!card_cords.empty()){ - for(auto one_cord:card_cords){ - if(0 == tableStrategyModel->current_player){ - range_number += tableStrategyModel->p1_range[one_cord.first][one_cord.second]; - }else{ - // when it's oop, which is p1 - range_number += tableStrategyModel->p2_range[one_cord.first][one_cord.second]; + // get range data + vector> card_cords; + card_cords = tableStrategyModel->ui_strategy_table[index.row()][index.column()]; + float range_number = 0; + if(!card_cords.empty()){ + for(auto one_cord:card_cords){ + if(0 == tableStrategyModel->current_player){ + range_number += tableStrategyModel->p1_range[one_cord.first][one_cord.second]; + }else{ + // when it's oop, which is p1 + range_number += tableStrategyModel->p2_range[one_cord.first][one_cord.second]; + } } + range_number = range_number / card_cords.size(); + if(range_number < 0 || range_number > 1) throw runtime_error("range number incorrect in strategyitemdeletage"); } - range_number = range_number / card_cords.size(); - if(range_number < 0 || range_number > 1) throw runtime_error("range number incorrect in strategyitemdeletage"); + float not_in_range = (tableStrategyModel->get_detail_window_setting()->grid_i == index.row()) && + (tableStrategyModel->get_detail_window_setting()->grid_j == index.column()) + ? 0 : 1 - range_number; + int niR_height = (int)(not_in_range * option.rect.height() * 0.95); + // got range data + + int disable_height = (int)(0.5+fold_prob * (option.rect.height() - niR_height)); + int remain_height = option.rect.height() - niR_height - disable_height; + + // draw background for flod + if (disable_height > 0) { + QRect rect(option.rect.left(), option.rect.top() + niR_height,\ + option.rect.width(), disable_height); + QBrush brush(QColor (0,191,255)); + painter->fillRect(rect, brush); } - float not_in_range = (this->detailWindowSetting->grid_i == index.row()) && - (this->detailWindowSetting->grid_j == index.column()) - ? 0 : 1 - range_number; - int niR_height = (int)(not_in_range * option.rect.height() * 0.95); - // got range data - - int disable_height = (int)(0.5+fold_prob * (option.rect.height() - niR_height)); - int remain_height = option.rect.height() - niR_height - disable_height; - - // draw background for flod - if (disable_height > 0) { - QRect rect(option.rect.left(), option.rect.top() + niR_height,\ - option.rect.width(), disable_height); - QBrush brush(QColor (0,191,255)); - painter->fillRect(rect, brush); - } - if (remain_height > 0){ - int ind = 0; - float last_prob = 0; - int bet_raise_num = 0; - for(std::size_t i = 0;i < strategy.size();i ++){ - GameActions one_action = strategy[i].first; - float normalized_ev = withEVs ? normalization_tanh(node->getPot() * 3, evs_without_fold[i]) : 1.; - QBrush brush(Qt::gray); - if(one_action.getAction() != GameTreeNode::PokerActions::FOLD){ - if(one_action.getAction() == GameTreeNode::PokerActions::CHECK - || one_action.getAction() == GameTreeNode::PokerActions::CALL){ - int green = 255; - int blue = max((int)(225 - normalized_ev * 175),55); - int red = 55; - brush = QBrush(QColor(red,green,blue)); + if (remain_height > 0){ + int ind = 0; + float last_prob = 0; + int bet_raise_num = 0; + for(std::size_t i = 0;i < strategy.size();i ++){ + GameActions one_action = strategy[i].first; + float normalized_ev = withEVs ? normalization_tanh(node->getPot() * 3, evs_without_fold[i]) : 1.; + QBrush brush(Qt::gray); + if(one_action.getAction() != GameTreeNode::PokerActions::FOLD){ + if(one_action.getAction() == GameTreeNode::PokerActions::CHECK + || one_action.getAction() == GameTreeNode::PokerActions::CALL){ + int green = 255; + int blue = max((int)(225 - normalized_ev * 175),55); + int red = 55; + brush = QBrush(QColor(red,green,blue)); + } + else if(one_action.getAction() == GameTreeNode::PokerActions::BET + || one_action.getAction() == GameTreeNode::PokerActions::RAISE){ + int color_base = max(128 - 32 * bet_raise_num - 1,0); + int blue = max((int)(255 - normalized_ev * (255 - color_base)), color_base); + int red = color_base + min((int)(normalized_ev * (255-color_base)),255-color_base);; + int green = color_base; + brush = QBrush(QColor(red,green,blue)); + + bet_raise_num += 1; + }else{ + brush = QBrush(Qt::blue); + } + + int delta_x = (int)(option.rect.width() * last_prob); + int delta_width = (int)(option.rect.width() * (last_prob + strategy_without_fold[ind])) - (int)(option.rect.width() * last_prob); + + QRect rect(option.rect.left() + delta_x, option.rect.top() + niR_height + disable_height,\ + delta_width , remain_height); + painter->fillRect(rect, brush); + + last_prob += strategy_without_fold[ind]; + ind += 1; } - else if(one_action.getAction() == GameTreeNode::PokerActions::BET - || one_action.getAction() == GameTreeNode::PokerActions::RAISE){ - int color_base = max(128 - 32 * bet_raise_num - 1,0); - int blue = max((int)(255 - normalized_ev * (255 - color_base)), color_base); - int red = color_base + min((int)(normalized_ev * (255-color_base)),255-color_base);; - int green = color_base; - brush = QBrush(QColor(red,green,blue)); - - bet_raise_num += 1; - }else{ - brush = QBrush(Qt::blue); - } - - int delta_x = (int)(option.rect.width() * last_prob); - int delta_width = (int)(option.rect.width() * (last_prob + strategy_without_fold[ind])) - (int)(option.rect.width() * last_prob); - - QRect rect(option.rect.left() + delta_x, option.rect.top() + niR_height + disable_height,\ - delta_width , remain_height); - painter->fillRect(rect, brush); - - last_prob += strategy_without_fold[ind]; - ind += 1; } } - } - } + } } QTextDocument doc; doc.setHtml(options.text); @@ -134,29 +135,32 @@ void StrategyItemDelegate::paint_range(QPainter *painter, const QStyleOptionView initStyleOption(&options, index); const TableStrategyModel * tableStrategyModel = qobject_cast(index.model()); + if (tableStrategyModel == nullptr) { + return; + } + vector> card_cords; - if(tableStrategyModel == NULL - || tableStrategyModel->ui_p1_range.size() <= index.row() - || tableStrategyModel->ui_p1_range.size() <= index.column() - || tableStrategyModel->ui_p2_range.size() <= index.row() - || tableStrategyModel->ui_p2_range.size() <= index.column() - ){ + if (static_cast(index.row()) >= tableStrategyModel->ui_p1_range.size() || + static_cast(index.row()) >= tableStrategyModel->ui_p2_range.size() || + static_cast(index.column()) >= tableStrategyModel->ui_p1_range[index.row()].size() || + static_cast(index.column()) >= tableStrategyModel->ui_p2_range[index.row()].size()) { return; } - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ + + if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ card_cords = tableStrategyModel->ui_p1_range[index.row()][index.column()]; }else{ card_cords = tableStrategyModel->ui_p2_range[index.row()][index.column()]; } - if(tableStrategyModel->p1_range.empty() || tableStrategyModel->p2_range.empty()){ + if (tableStrategyModel->p1_range.empty() || tableStrategyModel->p2_range.empty()) { return; } if(!card_cords.empty()){ float range_number = 0; for(auto one_cord:card_cords){ - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ + if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP){ range_number += tableStrategyModel->p1_range[one_cord.first][one_cord.second]; }else{ // when it's oop, which is p1 @@ -192,13 +196,17 @@ void StrategyItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewIt auto options = option; initStyleOption(&options, index); const TableStrategyModel * tableStrategyModel = qobject_cast(index.model()); + if (!tableStrategyModel || !tableStrategyModel->get_qsolverjob()) { + return; + } + vector evs = tableStrategyModel->get_ev_grid(index.row(),index.column()); sort(evs.begin(), evs.end()); int last_left = 0; for(std::size_t i = 0;i < evs.size();i ++ ){ float one_ev = evs[evs.size() - i - 1]; - float normalized_ev = normalization_tanh(this->qSolverJob->stack,one_ev); + float normalized_ev = normalization_tanh(tableStrategyModel->get_qsolverjob()->stack,one_ev); //options.text += QString("
%1").arg(QString::number(normalized_ev)); int red = max((int)(255 - normalized_ev * 255),0); @@ -234,17 +242,23 @@ void StrategyItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem & if(index.column() == index.row())brush = QBrush(Qt::darkGray); painter->fillRect(rect, brush); - if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::STRATEGY){ + const TableStrategyModel * tableStrategyModel = qobject_cast(index.model()); + if (!tableStrategyModel || !tableStrategyModel->get_detail_window_setting()) { + painter->restore(); + return; + } + + if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::STRATEGY){ this->paint_strategy(painter,option,index); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP || - this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_OOP ){ + else if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP || + tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::RANGE_OOP ){ this->paint_range(painter,option,index); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::EV){ + else if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::EV){ this->paint_strategy(painter,option,index,true); } - else if(this->detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::EV_ONLY){ + else if(tableStrategyModel->get_detail_window_setting()->mode == DetailWindowSetting::DetailWindowMode::EV_ONLY){ this->paint_evs(painter,option,index); } diff --git a/src/ui/tablestrategymodel.cpp b/src/ui/tablestrategymodel.cpp index 99d67817..04951801 100644 --- a/src/ui/tablestrategymodel.cpp +++ b/src/ui/tablestrategymodel.cpp @@ -1,503 +1,372 @@ #include "include/ui/tablestrategymodel.h" +#include "include/nodes/ActionNode.h" +#include "include/solver/Solver.h" +#include "include/runtime/PokerSolver.h" -TableStrategyModel::TableStrategyModel(QSolverJob * data, QObject *parent) - : QAbstractItemModel(parent) +TableStrategyModel::TableStrategyModel(QSolverJob *qSolverJob, DetailWindowSetting* setting, QObject *parent) + : QAbstractItemModel(parent), qSolverJob(qSolverJob), detailWindowSetting(setting) { - this->qSolverJob = data; - setupModelData(); + // This is the crucial part that was missing. + // We need to populate the cardint2card map so that the UI can later + // look up card objects from their integer IDs to display their names. + this->cardint2card.assign(52, Card()); + if (this->qSolverJob && this->qSolverJob->get_solver()) { + Deck* deck = this->qSolverJob->get_solver()->get_deck(); + if (deck) { + for (const Card& card : deck->getCards()) { + int card_int = card.getCardInt(); + if (card_int >= 0 && card_int < 52) { + this->cardint2card[card_int] = card; + } + } + } + } + int valid_cards = 0; + for(const auto& card : this->cardint2card) { + if (!card.empty()) { + valid_cards++; + } + } + qDebug() << "[DEBUG] TableStrategyModel: Populated cardint2card with" << valid_cards << "cards."; + + this->ranklist = (QStringList() << "A" << "K" << "Q" << "J" << "T" << "9" << "8" << "7" << "6" << "5" << "4" << "3" << "2"); + build_ui_tables(); } -TableStrategyModel::~TableStrategyModel() +QVariant TableStrategyModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return QVariant(); + + if (role == Qt::DisplayRole) { + // This model primarily holds data, display is handled by the delegate + return QVariant(); + } + + return QVariant(); } QModelIndex TableStrategyModel::index(int row, int column, const QModelIndex &parent) const { - if (!hasIndex(row, column, parent)) + if (parent.isValid()) return QModelIndex(); - - return createIndex(row, column, nullptr); -} - -QVariant TableStrategyModel::headerData(int section, Qt::Orientation orientation, int role){ - return QString::fromStdString(""); + return createIndex(row, column); } -QModelIndex TableStrategyModel::parent(const QModelIndex &child) const{ +QModelIndex TableStrategyModel::parent(const QModelIndex &index) const +{ return QModelIndex(); } -int TableStrategyModel::columnCount(const QModelIndex &parent) const +int TableStrategyModel::rowCount(const QModelIndex &parent) const { - return this->qSolverJob->get_solver()->get_deck()->getRanks().size(); + if (parent.isValid()) + return 0; + return 13; } -int TableStrategyModel::rowCount(const QModelIndex &parent) const +int TableStrategyModel::columnCount(const QModelIndex &parent) const { - return this->qSolverJob->get_solver()->get_deck()->getRanks().size(); + if (parent.isValid()) + return 0; + return 13; } -QVariant TableStrategyModel::data(const QModelIndex &index, int role) const -{ - vectorranks = this->qSolverJob->get_solver()->get_deck()->getRanks(); - int row = index.row(); - int col = index.column(); - int larger = row > col?row:col; - int smaller = row > col?col:row; - QString retval = QString("

%1%2%3

")\ - .arg(QString::fromStdString(ranks[ranks.size() - 1 - smaller]))\ - .arg(QString::fromStdString(ranks[ranks.size() - 1 - larger])); - if(row > col){ - retval = retval.arg(tr("o")); - }else if(row < col){ - retval = retval.arg(tr("s")); - }else{ - retval = retval.arg(tr(" ")); - } - return retval; +void TableStrategyModel::setGameTreeNode(const weak_ptr& node) { + this->currentNode = node; } -void TableStrategyModel::setupModelData() -{ - vector cards = this->qSolverJob->get_solver()->get_deck()->getCards(); - vector ranks = this->qSolverJob->get_solver()->get_deck()->getRanks(); - for(auto one_card: cards){ - this->cardint2card.insert(std::pair(one_card.getCardInt(), one_card)); +void TableStrategyModel::updateStrategyData() { + beginResetModel(); + total_strategy.clear(); + shared_ptr node = currentNode.lock(); + if (!node) { + current_strategy.clear(); + current_evs.clear(); + endResetModel(); + return; } - this->ui_strategy_table = vector>>>(ranks.size()); - for(std::size_t i = 0;i < ranks.size();i ++){ - this->ui_strategy_table[i] = vector>>(ranks.size()); - for(std::size_t j = 0;j < ranks.size();j ++){ - this->ui_strategy_table[i][j] = vector>(); - } + if (node->getType() != GameTreeNode::ACTION) { + current_strategy.clear(); + current_evs.clear(); + endResetModel(); + return; } - this->p1_range = vector>(52); - for(std::size_t i = 0;i < 52;i ++){ - this->p1_range[i] = vector(52); - for(std::size_t j = 0;j < 52;j ++){ - this->p1_range[i][j] = 0; - } - } + shared_ptr actionNode = static_pointer_cast(node); + current_player = actionNode->getPlayer(); - this->p2_range = vector>(52); - for(std::size_t i = 0;i < 52;i ++){ - this->p2_range[i] = vector(52); - for(std::size_t j = 0;j < 52;j ++){ - this->p2_range[i][j] = 0; - } + shared_ptr solver = qSolverJob->get_solver()->get_solver(); + if (!solver) { + current_strategy.clear(); + current_evs.clear(); + endResetModel(); + return; } - this->ui_p1_range = vector>>>(ranks.size()); - for(std::size_t i = 0;i < ranks.size();i ++){ - this->ui_p1_range[i] = vector>>(ranks.size()); - for(std::size_t j = 0;j < ranks.size();j ++){ - this->ui_p1_range[i][j] = vector>(); + vector chance_cards; + GameTreeNode::GameRound round = actionNode->getRound(); + + if (round == GameTreeNode::GameRound::TURN) { + if (!turnCard.empty()) { + chance_cards.push_back(turnCard); + } + } else if (round == GameTreeNode::GameRound::RIVER) { + if (!riverCard.empty()) { + chance_cards.push_back(riverCard); } } - this->ui_p2_range = vector>>>(ranks.size()); - for(std::size_t i = 0;i < ranks.size();i ++){ - this->ui_p2_range[i] = vector>>(ranks.size()); - for(std::size_t j = 0;j < ranks.size();j ++){ - this->ui_p2_range[i][j] = vector>(); - } + current_strategy = solver->get_strategy(actionNode, chance_cards); + current_evs = solver->get_evs(actionNode, chance_cards); + + qDebug() << "[DEBUG] updateStrategyData: Received data from solver for node of type" << node->getType(); + qDebug() << " - current_strategy size:" << current_strategy.size(); + if (!current_strategy.empty()) { + qDebug() << " - current_strategy[0] size:" << current_strategy[0].size(); + } + qDebug() << " - current_evs size:" << current_evs.size(); + if (!current_evs.empty()) { + qDebug() << " - current_evs[0] size:" << current_evs[0].size(); } - vector& p1range = this->qSolverJob->get_solver()->player1Range; - vector& p2range = this->qSolverJob->get_solver()->player2Range; + // Determine which player's range to display based on the UI mode. + // This is the fix for the "0 combos found" bug. The previous logic was + // incomplete as it did not account for the user's selection. + int player_to_display = -1; + if (detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_IP) { + player_to_display = 0; // IP is always player 0 + } else if (detailWindowSetting->mode == DetailWindowSetting::DetailWindowMode::RANGE_OOP) { + player_to_display = 1; // OOP is always player 1 + } else { + // For STRATEGY, EV, EV_ONLY modes, display the range of the current acting player. + player_to_display = current_player; + } - for(PrivateCards one_private: p1range){ - Card card1 = this->cardint2card[one_private.card1]; - Card card2 = this->cardint2card[one_private.card2]; + // Set the active strategy table based on the player to display. + this->ui_strategy_table = (player_to_display == 0) + ? this->ui_p1_range + : this->ui_p2_range; - int rank1 = card1.getCardInt() / 4; - int suit1 = card1.getCardInt() - (rank1)*4; - int index1 = 12 - rank1; // this index is the index of the actal ui, so AKQ would be the lower index and 234 would be high + // Calculate the aggregated "total_strategy" for the rough view + vector>>& ev_data = current_evs; + vector>>& strat_data = current_strategy; - int rank2 = card2.getCardInt() / 4; - int suit2 = card2.getCardInt() - (rank2)*4; - int index2 = 12 - rank2; + // To calculate the full rough strategy, we need both EV and strategy percentages. + if (strat_data.empty() || ev_data.empty()) { + endResetModel(); + return; + } - if(index1 == index2){ - this->ui_p1_range[index1][index2].push_back(std::pair(one_private.card1,one_private.card2)); - } - else if(suit1 == suit2){ - this->ui_p1_range[min(index1,index2)][max(index1,index2)].push_back(std::pair(one_private.card1,one_private.card2)); - }else{ - this->ui_p1_range[max(index1,index2)][min(index1,index2)].push_back(std::pair(one_private.card1,one_private.card2)); - } + vector& actions = actionNode->getActions(); + int player = actionNode->getPlayer(); + PokerSolver* ps = qSolverJob->get_solver(); + if (!ps) { + endResetModel(); + return; } - for(PrivateCards one_private: p2range){ - Card card1 = this->cardint2card[one_private.card1]; - Card card2 = this->cardint2card[one_private.card2]; + const vector& range = (player == 0) ? ps->player1Range : ps->player2Range; - int rank1 = card1.getCardInt() / 4; - int suit1 = card1.getCardInt() - (rank1)*4; - int index1 = 12 - rank1; // this index is the index of the actal ui, so AKQ would be the lower index and 234 would be high + vector> summed_values(actions.size(), {0.0f, 0.0f}); // {EV, Strat} + float total_weight = 0.0f; - int rank2 = card2.getCardInt() / 4; - int suit2 = card2.getCardInt() - (rank2)*4; - int index2 = 12 - rank2; + for (const auto& pc : range) { + if (pc.card1 >= 52 || pc.card2 >= 52 || pc.card1 < 0 || pc.card2 < 0) continue; - if(index1 == index2){ - this->ui_p2_range[index1][index2].push_back(std::pair(one_private.card1,one_private.card2)); + if (static_cast(pc.card1) >= strat_data.size() || + static_cast(pc.card2) >= strat_data[pc.card1].size() || + strat_data[pc.card1][pc.card2].empty() || + static_cast(pc.card1) >= ev_data.size() || + static_cast(pc.card2) >= ev_data[pc.card1].size() || + ev_data[pc.card1][pc.card2].empty()) { + continue; } - else if(suit1 == suit2){ - this->ui_p2_range[min(index1,index2)][max(index1,index2)].push_back(std::pair(one_private.card1,one_private.card2)); - }else{ - this->ui_p2_range[max(index1,index2)][min(index1,index2)].push_back(std::pair(one_private.card1,one_private.card2)); + const auto& hand_strat = strat_data[pc.card1][pc.card2]; + const auto& hand_ev = ev_data[pc.card1][pc.card2]; + if (hand_strat.size() != actions.size() || hand_ev.size() != actions.size()) continue; + + total_weight += pc.weight; + for (size_t i = 0; i < actions.size(); ++i) { + summed_values[i].first += hand_ev[i] * pc.weight; // EV + summed_values[i].second += hand_strat[i] * pc.weight; // Strategy } } - this->total_strategy = vector>>(); -} - -void TableStrategyModel::clicked_event(const QModelIndex & index){ -} -void TableStrategyModel::setGameTreeNode(TreeItem* treeNode){ - this->treeItem = treeNode; -} - -void TableStrategyModel::setTrunCard(Card turn_card){ - this->turn_card = turn_card; -} - -void TableStrategyModel::setRiverCard(Card river_card){ - this->river_card = river_card; -} - -void TableStrategyModel::updateStrategyData(){ - if(this->treeItem != NULL){ - shared_ptr node = this->treeItem->m_treedata.lock(); - this->setupModelData(); - if(node != nullptr && node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(node); - //actionNode->getTrainable(); - // create a vector card and input it to solver and get the result - vector deal_cards; - GameTreeNode::GameRound root_round = this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound(); - GameTreeNode::GameRound current_round = actionNode->getRound(); - this->current_player = actionNode->getPlayer(); - if(root_round == GameTreeNode::GameRound::FLOP){ - if(current_round == GameTreeNode::GameRound::TURN){deal_cards.push_back(this->turn_card);} - if(current_round == GameTreeNode::GameRound::RIVER){deal_cards.push_back(this->turn_card);deal_cards.push_back(this->river_card);} - } - else if(root_round == GameTreeNode::GameRound::TURN){ - if(current_round == GameTreeNode::GameRound::RIVER){deal_cards.push_back(this->river_card);} - } - if(this->qSolverJob->get_solver() != NULL && this->qSolverJob->get_solver()->get_solver() != NULL){ - vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(actionNode,deal_cards); - this->current_strategy = current_strategy; - - vector>> current_evs = this->qSolverJob->get_solver()->get_solver()->get_evs(actionNode,deal_cards); - this->current_evs = current_evs; - - for(int i = 0;i < 52;i ++){ - for(int j = 0;j < 52;j ++){ - const vector& one_strategy = this->current_strategy[i][j]; - if(one_strategy.empty())continue; - Card card1 = this->cardint2card[i]; - Card card2 = this->cardint2card[j]; - - int rank1 = card1.getCardInt() / 4; - int suit1 = card1.getCardInt() - (rank1)*4; - int index1 = 12 - rank1; // this index is the index of the actal ui, so AKQ would be the lower index and 234 would be high - - int rank2 = card2.getCardInt() / 4; - int suit2 = card2.getCardInt() - (rank2)*4; - int index2 = 12 - rank2; - - if(index1 == index2){ - this->ui_strategy_table[index1][index2].push_back(std::pair(i,j)); - } - else if(suit1 == suit2){ - this->ui_strategy_table[min(index1,index2)][max(index1,index2)].push_back(std::pair(i,j)); - }else{ - this->ui_strategy_table[max(index1,index2)][min(index1,index2)].push_back(std::pair(i,j)); - } - } - } - } + if (total_weight > 0) { + for (size_t i = 0; i < actions.size(); ++i) { + summed_values[i].first /= total_weight; // Average EV + summed_values[i].second /= total_weight; // Average probability + total_strategy.push_back({actions[i], summed_values[i]}); } - if(this->qSolverJob->get_solver() != NULL && this->qSolverJob->get_solver()->get_solver() != NULL && node != nullptr){ - vector& p1range = this->qSolverJob->get_solver()->player1Range; - vector& p2range = this->qSolverJob->get_solver()->player2Range; - - for(auto one_private:p1range)this->p1_range[one_private.card1][one_private.card2] = one_private.weight; - for(auto one_private:p2range)this->p2_range[one_private.card1][one_private.card2] = one_private.weight; - - shared_ptr iter_node = node->getParent(); - shared_ptr last_node = node; - while(iter_node != nullptr){ - if(iter_node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr iterActionNode = dynamic_pointer_cast(iter_node); - vector deal_cards; - GameTreeNode::GameRound root_round = this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound(); - GameTreeNode::GameRound current_round = iterActionNode->getRound(); - if(root_round == GameTreeNode::GameRound::FLOP){ - if(current_round == GameTreeNode::GameRound::TURN){deal_cards.push_back(this->turn_card);} - if(current_round == GameTreeNode::GameRound::RIVER){deal_cards.push_back(this->turn_card);deal_cards.push_back(this->river_card);} - } - else if(root_round == GameTreeNode::GameRound::TURN){ - if(current_round == GameTreeNode::GameRound::RIVER){deal_cards.push_back(this->river_card);} - } - - vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(iterActionNode,deal_cards); - - int child_chosen = -1; - for(std::size_t i = 0;i < iterActionNode->getChildrens().size();i ++){ - if(iterActionNode->getChildrens()[i] == last_node){ - child_chosen = i; - break; - } - } - if(child_chosen == -1)throw runtime_error("no child chosen"); - for(std::size_t i = 0;i < 52;i ++){ - for(std::size_t j = 0;j < 52;j ++){ - if(current_strategy[i][j].size() == 0)continue; - if(iterActionNode->getPlayer() == 0){ // p1, IP - this->p1_range[i][j] *= current_strategy[i][j][child_chosen]; - } - else if(iterActionNode->getPlayer() == 1){ // p2, OOP - this->p2_range[i][j] *= current_strategy[i][j][child_chosen]; - }else throw runtime_error("player not exist in tablestrategymodel"); - } - } - } + } + endResetModel(); +} - iter_node = iter_node->getParent(); - last_node = last_node->getParent(); +void TableStrategyModel::build_ui_tables() { + ui_strategy_table.assign(13, vector>>(13)); + ui_p1_range.assign(13, vector>>(13)); + ui_p2_range.assign(13, vector>>(13)); + p1_range.assign(52, vector(52, 0.0f)); + p2_range.assign(52, vector(52, 0.0f)); + + for(int i=0; i<13; i++) { + for(int j=0; j<13; j++) { + string hand_str; + if (i < j) { // Suited + hand_str = ranklist[i].toStdString() + ranklist[j].toStdString() + "s"; + } else if (i > j) { // Offsuit + hand_str = ranklist[j].toStdString() + ranklist[i].toStdString() + "o"; + } else { // Pair + hand_str = ranklist[i].toStdString() + ranklist[j].toStdString(); } - } - if(this->qSolverJob->get_solver() != NULL && this->qSolverJob->get_solver()->get_solver() != NULL && node != nullptr){ - this->total_strategy = this->get_total_strategy(); + string2ij[hand_str] = {i, j}; } } -} - -const vector>> TableStrategyModel::get_total_strategy() const{ - vector>> ret_strategy; - if(this->treeItem == NULL)return ret_strategy; - - - shared_ptr node = this->treeItem->m_treedata.lock(); - - if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(node); - vector& gameActions = actionNode->getActions(); - int current_player = actionNode->getPlayer(); - - vector combos(gameActions.size(),0.0); - vector avg_strategy(gameActions.size(),0.0); - float sum_strategy = 0; - for(std::size_t index1 = 0;index1 < this->current_strategy.size() ;index1 ++){ - for(std::size_t index2 = 0;index2 < this->current_strategy.size() ;index2 ++){ - const vector& one_strategy = this->current_strategy[index1][index2]; - if(one_strategy.empty())continue; + PokerSolver* ps = qSolverJob->get_solver(); + if (!ps) return; - const vector>& range = current_player == 0? this->p1_range:this->p2_range; - if(range.size() <= index1 || range[index1].size() < index2) throw runtime_error(" index error when get range in tablestrategymodel"); - const float one_range = range[index1][index2]; + const auto& p1_range_vec = ps->player1Range; + const auto& p2_range_vec = ps->player2Range; - for(std::size_t i = 0;i < one_strategy.size(); i ++ ){ - float one_prob = one_strategy[i]; - combos[i] += one_prob * one_range; - avg_strategy[i] += one_prob * one_range; - sum_strategy += one_prob * one_range; - } - - if(gameActions.size() != one_strategy.size()){ - cout << "index: " << index1 << " " << index2 << endl; - cout << "size not match between gameAction and stragegy: " << gameActions.size() << " " << one_strategy.size() << endl; - throw runtime_error("size not match between gameAction and stragegy"); - } - } + for(const auto& pc : p1_range_vec) { + p1_range[pc.card1][pc.card2] = pc.weight; + p1_range[pc.card2][pc.card1] = pc.weight; + string hand_str = PrivateCards(pc.card1, pc.card2, 0).toString(); + if (string2ij.count(hand_str)) { + auto& pos = string2ij[hand_str]; + ui_p1_range[pos.first][pos.second].push_back({pc.card1, pc.card2}); } + } - for(std::size_t i = 0;i < gameActions.size(); i ++ ){ - avg_strategy[i] = avg_strategy[i] / sum_strategy; - pair statics = pair(combos[i],avg_strategy[i]); - pair> one_ret = pair>(gameActions[i],statics); - ret_strategy.push_back(one_ret); + for(const auto& pc : p2_range_vec) { + p2_range[pc.card1][pc.card2] = pc.weight; + p2_range[pc.card2][pc.card1] = pc.weight; + string hand_str = PrivateCards(pc.card1, pc.card2, 0).toString(); + if (string2ij.count(hand_str)) { + auto& pos = string2ij[hand_str]; + ui_p2_range[pos.first][pos.second].push_back({pc.card1, pc.card2}); } - return ret_strategy; - }else{ - return ret_strategy; } - + qDebug() << "[DEBUG] TableStrategyModel: Built UI tables. Example cell sizes:"; + qDebug() << " - P1 AA (0,0) combos:" << ui_p1_range[0][0].size(); + qDebug() << " - P1 AKs (0,1) combos:" << ui_p1_range[0][1].size(); + qDebug() << " - P2 77 (7,7) combos:" << ui_p2_range[7][7].size(); + qDebug() << " - P2 T9s (4,5) combos:" << ui_p2_range[4][5].size(); } -const vector> TableStrategyModel::get_strategy(int i,int j) const{ - vector> ret_strategy; - if(this->treeItem == NULL) return ret_strategy; - - int strategy_number = this->ui_strategy_table[i][j].size(); - - shared_ptr node = this->treeItem->m_treedata.lock(); - - if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(node); - - vector& gameActions = actionNode->getActions(); - - vector strategies; - - // get range data - initally copied from paint_range - could probably be integrated in loops below for efficiancy - vector> card_cords; - const vector> *current_range; - card_cords = ui_strategy_table[i][j]; - current_range = (0 == current_player ) ? & p1_range : & p2_range; - - if(p1_range.empty() || p2_range.empty()){ - return ret_strategy; - } - - float range_number = 0; - if(!card_cords.empty()){ - for(std::size_t indi = 0;indi < card_cords.size();indi ++){ - range_number += (*current_range)[card_cords[indi].first][card_cords[indi].second]; - } - range_number = range_number / card_cords.size(); - - if(range_number < 0 || range_number > 1) throw runtime_error("range number incorrect in strategyitemdeletage"); - } - else - return ret_strategy; - // got range data - - if(this->ui_strategy_table[i][j].size() > 0){ - strategies = vector(gameActions.size()); - std::fill(strategies.begin(), strategies.end(), 0.); - } - for(std::pair index:this->ui_strategy_table[i][j]){ - int index1 = index.first; - int index2 = index.second; - const vector& one_strategy = this->current_strategy[index1][index2]; - if(gameActions.size() != one_strategy.size()){ - cout << "index: " << index1 << " " << index2 << endl; - cout << "i,j: " << i << " " << j << endl; - cout << "size not match between gameAction and stragegy: " << gameActions.size() << " " << one_strategy.size() << endl; - throw runtime_error("size not match between gameAction and stragegy"); +vector TableStrategyModel::get_ev_grid(int i, int j) const { + vector evs; + if (i < 0 || i >= 13 || j < 0 || j >= 13) return evs; + + const auto& combos = ui_strategy_table[i][j]; + for (const auto& combo : combos) { + if (current_evs.empty() || static_cast(combo.first) >= current_evs.size() || static_cast(combo.second) >= current_evs[combo.first].size() || current_evs[combo.first][combo.second].empty()) { + evs.push_back(0.0f); // Or some indicator for no data + } else { + float total_ev = 0.0f; + for (float ev : current_evs[combo.first][combo.second]) { + total_ev += ev; } - - if ( range_number > 0) - for(std::size_t indi = 0;indi < one_strategy.size();indi ++){ - strategies[indi] += (one_strategy[indi] * (*current_range)[index1][index2] / range_number / strategy_number); - } + evs.push_back(total_ev); } - - for(std::size_t indi = 0;indi < strategies.size();indi ++){ - ret_strategy.push_back(std::pair(actionNode->getActions()[indi], - strategies[indi])); - } - - return ret_strategy; - }else{ - return ret_strategy; } - + return evs; } -const vector TableStrategyModel::get_ev_grid(int i,int j)const{ - vector ret_evs; - if(this->treeItem == NULL || this->current_evs.empty())return ret_evs; +void TableStrategyModel::setTrunCard(const Card &card) { this->turnCard = card; } +void TableStrategyModel::setRiverCard(const Card &card) { this->riverCard = card; } +Card TableStrategyModel::getTrunCard() const { return this->turnCard; } +Card TableStrategyModel::getRiverCard() const { return this->riverCard; } - int strategy_number = this->ui_strategy_table[i][j].size(); - - shared_ptr node = this->treeItem->m_treedata.lock(); +vector> TableStrategyModel::get_strategy(int i, int j) const { + vector> avg_strategy; + shared_ptr node = currentNode.lock(); + if (i < 0 || i >= 13 || j < 0 || j >= 13 || !node) { + return avg_strategy; + } - if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(node); + if (node->getType() != GameTreeNode::ACTION) { + return avg_strategy; + } + shared_ptr actionNode = static_pointer_cast(node); + vector& actions = actionNode->getActions(); - vector& gameActions = actionNode->getActions(); + const auto& combos = ui_strategy_table[i][j]; + if (combos.empty() || current_strategy.empty()) { + return avg_strategy; + } -// vector strategies; + vector summed_strats(actions.size(), 0.0f); + int combo_count = 0; -// if(this->ui_strategy_table[i][j].size() > 0){ -// strategies = vector(gameActions.size()); -// std::fill(strategies.begin(), strategies.end(), 0.); -// } - for(std::pair index:this->ui_strategy_table[i][j]){ - int index1 = index.first; - int index2 = index.second; - const vector& one_strategy = this->current_strategy[index1][index2]; - const vector& one_ev = this->current_evs[index1][index2]; - if(one_ev.size() != one_strategy.size()) return vector(); + for (const auto& combo : combos) { + if (static_cast(combo.first) < current_strategy.size() && + static_cast(combo.second) < current_strategy[combo.first].size() && + !current_strategy[combo.first][combo.second].empty()) { - if(gameActions.size() != one_strategy.size()){ - cout << "index: " << index1 << " " << index2 << endl; - cout << "i,j: " << i << " " << j << endl; - cout << "size not match between gameAction and stragegy: " << gameActions.size() << " " << one_strategy.size() << endl; - throw runtime_error("size not match between gameAction and stragegy"); - } - float one_ev_float = 0; - for(std::size_t indi = 0;indi < one_strategy.size();indi ++){ - one_ev_float += one_strategy[indi] * one_ev[indi]; + const auto& hand_strat = current_strategy[combo.first][combo.second]; + if (hand_strat.size() == actions.size()) { + for (size_t k = 0; k < actions.size(); ++k) { + summed_strats[k] += hand_strat[k]; + } + combo_count++; } - ret_evs.push_back(one_ev_float); } + } - return ret_evs; - }else{ - return ret_evs; + if (combo_count > 0) { + for (size_t k = 0; k < actions.size(); ++k) { + avg_strategy.push_back({actions[k], summed_strats[k] / combo_count}); + } } + + return avg_strategy; } +vector TableStrategyModel::get_strategies_evs(int i, int j) const { + vector avg_evs; + shared_ptr node = currentNode.lock(); + if (i < 0 || i >= 13 || j < 0 || j >= 13 || !node) { + return avg_evs; + } -const vector TableStrategyModel::get_strategies_evs(int i,int j)const{ - vector ret_evs; - if(this->treeItem == NULL || this->current_evs.empty())return ret_evs; + if (node->getType() != GameTreeNode::ACTION) { + return avg_evs; + } + shared_ptr actionNode = static_pointer_cast(node); + vector& actions = actionNode->getActions(); - const vector> *current_range; - current_range = (0 == current_player ) ? & p1_range : & p2_range; - if(p1_range.empty() || p2_range.empty()){ - return ret_evs; + const auto& combos = ui_strategy_table[i][j]; + if (combos.empty() || current_evs.empty()) { + return avg_evs; } - shared_ptr node = this->treeItem->m_treedata.lock(); + vector summed_evs(actions.size(), 0.0f); + int combo_count = 0; - if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(node); - vector& gameActions = actionNode->getActions(); + for (const auto& combo : combos) { + if (static_cast(combo.first) < current_evs.size() && + static_cast(combo.second) < current_evs[combo.first].size() && + !current_evs[combo.first][combo.second].empty()) { - vector strategy_p; - if(this->ui_strategy_table[i][j].size() > 0){ - ret_evs = vector(gameActions.size()); - std::fill(ret_evs.begin(), ret_evs.end(), 0.); - strategy_p = vector(gameActions.size()); - std::fill(strategy_p.begin(), strategy_p.end(), 0.); - } - float range = 0; - for(std::pair index:this->ui_strategy_table[i][j]){ - int index1 = index.first; - int index2 = index.second; - const vector& one_strategy = this->current_strategy[index1][index2]; - const vector& one_ev = this->current_evs[index1][index2]; - const float one_range = (*current_range)[index1][index2]; - if(gameActions.size() != one_strategy.size() || one_ev.size() != one_strategy.size()){ - cout << "index: " << index1 << " " << index2 << endl; - cout << "i,j: " << i << " " << j << endl; - cout << "size not match between one_ev, gameAction and one_stragegy: " - << one_ev.size() << " " << gameActions.size() << " " << one_strategy.size() << endl; - throw runtime_error("size not match between one_ev, gameAction and one_stragegy"); - } - for(std::size_t indi = 0;indi < ret_evs.size();indi ++){ - ret_evs[indi] += one_strategy[indi] * one_ev[indi] * one_range; - strategy_p[indi] += one_strategy[indi] * one_range; + const auto& hand_ev = current_evs[combo.first][combo.second]; + if (hand_ev.size() == actions.size()) { + for (size_t k = 0; k < actions.size(); ++k) { + summed_evs[k] += hand_ev[k]; + } + combo_count++; } - range += one_range; } - for(std::size_t indi = 0;indi < ret_evs.size();indi ++){ - if (strategy_p[indi] > 0. && range > 0.) { - ret_evs[indi] = ret_evs[indi] / strategy_p[indi]; - } + } + + if (combo_count > 0) { + for (size_t k = 0; k < actions.size(); ++k) { + avg_evs.push_back(summed_evs[k] / combo_count); } - return ret_evs; - }else{ - return ret_evs; } -} + + return avg_evs; +} \ No newline at end of file diff --git a/src/ui/treeitem.cpp b/src/ui/treeitem.cpp index d7f53033..1eaba7f2 100644 --- a/src/ui/treeitem.cpp +++ b/src/ui/treeitem.cpp @@ -52,8 +52,13 @@ QString TreeItem::get_game_action_str(GameTreeNode::PokerActions action,float am QVariant TreeItem::data() const { - shared_ptr parentNode = this->m_treedata.lock()->getParent(); shared_ptr currentNode = this->m_treedata.lock(); + if (!currentNode) { + return "Invalid Node"; + } + + shared_ptr parentNode = currentNode->getParent(); + if(parentNode == nullptr){ return TreeItem::get_round_str(currentNode->getRound()) + QObject::tr(" begin"); } @@ -63,7 +68,6 @@ QVariant TreeItem::data() const vector>& childrens = parentActionNode->getChildrens(); for(std::size_t i = 0;i < childrens.size();i ++){ if(childrens[i] == currentNode){ - float amount = childrens[i]->getPot() - parentNode->getPot(); return (parentActionNode->getPlayer() == 0 ? QObject::tr("IP "):QObject::tr("OOP ")) + \ TreeItem::get_game_action_str(actions[i].getAction(),actions[i].getAmount()); } diff --git a/strategyexplorer.cpp b/strategyexplorer.cpp index 0f471625..47323d1c 100644 --- a/strategyexplorer.cpp +++ b/strategyexplorer.cpp @@ -1,11 +1,9 @@ #include "strategyexplorer.h" #include "ui_strategyexplorer.h" #include "qstandarditemmodel.h" -#include -#include #include -#include #include "include/Card.h" +#include "include/solver/PCfrSolver.h" StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : QDialog(parent), @@ -38,45 +36,13 @@ StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : ); // Initize strategy(rough) table - this->tableStrategyModel = new TableStrategyModel(this->qSolverJob,this); + this->tableStrategyModel = new TableStrategyModel(this->qSolverJob, &(this->detailWindowSetting), this); this->ui->strategyTableView->setModel(this->tableStrategyModel); - this->delegate_strategy = new StrategyItemDelegate(this->qSolverJob,&(this->detailWindowSetting),this); + this->delegate_strategy = new StrategyItemDelegate(this); this->ui->strategyTableView->setItemDelegate(this->delegate_strategy); - Deck* deck = this->qSolverJob->get_solver()->get_deck(); - int index = 0; - QString board_qstring = QString::fromStdString(this->qSolverJob->board); - for(Card one_card: deck->getCards()){ - if(board_qstring.contains(QString::fromStdString(one_card.toString())))continue; - QString card_str_formatted = QString::fromStdString(one_card.toFormattedString()); - this->ui->turnCardBox->addItem(card_str_formatted); - this->ui->riverCardBox->addItem(card_str_formatted); - - if(card_str_formatted.contains(QString::fromLocal8Bit("♦️")) || - card_str_formatted.contains(QString::fromLocal8Bit("♥️️"))){ - this->ui->turnCardBox->setItemData(0, QBrush(Qt::red),Qt::ForegroundRole); - this->ui->riverCardBox->setItemData(0, QBrush(Qt::red),Qt::ForegroundRole); - }else{ - this->ui->turnCardBox->setItemData(0, QBrush(Qt::black),Qt::ForegroundRole); - this->ui->riverCardBox->setItemData(0, QBrush(Qt::black),Qt::ForegroundRole); - } - - this->cards.push_back(one_card); - index += 1; - } - if(this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound() == GameTreeNode::GameRound::FLOP){ - this->tableStrategyModel->setTrunCard(this->cards[0]); - this->tableStrategyModel->setRiverCard(this->cards[1]); - this->ui->riverCardBox->setCurrentIndex(1); - } - else if(this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound() == GameTreeNode::GameRound::TURN){ - this->tableStrategyModel->setRiverCard(this->cards[0]); - this->ui->turnCardBox->clear(); - } - else if(this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound() == GameTreeNode::GameRound::RIVER){ - this->ui->turnCardBox->clear(); - this->ui->riverCardBox->clear(); - } + // Initialize turn and river card selectors based on solver mode + initializeView(); // Initize timer for strategy auto update timer = new QTimer(this); @@ -89,14 +55,26 @@ StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : // Initize Detail Viewer window this->detailViewerModel = new DetailViewerModel(this->tableStrategyModel,this); this->ui->detailView->setModel(this->detailViewerModel); - this->detailItemItemDelegate = new DetailItemDelegate(&(this->detailWindowSetting),this); + this->detailItemItemDelegate = new DetailItemDelegate(this); this->ui->detailView->setItemDelegate(this->detailItemItemDelegate); // Initize Rough Strategy Viewer this->roughStrategyViewerModel = new RoughStrategyViewerModel(this->tableStrategyModel,this); this->ui->roughStrategyView->setModel(this->roughStrategyViewerModel); - this->roughStrategyItemDelegate = new RoughStrategyItemDelegate(&(this->detailWindowSetting),this); + this->roughStrategyItemDelegate = new RoughStrategyItemDelegate(this); this->ui->roughStrategyView->setItemDelegate(this->roughStrategyItemDelegate); + + // Programmatically select the root node to ensure the view is populated on startup. + QAbstractItemModel* treeModel = this->ui->gameTreeView->model(); + if (treeModel && treeModel->rowCount() > 0) { + QModelIndex rootIndex = treeModel->index(0, 0, QModelIndex()); + if (rootIndex.isValid()) { + // Set the current selection in the tree view + this->ui->gameTreeView->setCurrentIndex(rootIndex); + // Manually call the click handler to load the data for the root node + item_clicked(rootIndex); + } + } } StrategyExplorer::~StrategyExplorer() @@ -105,10 +83,92 @@ StrategyExplorer::~StrategyExplorer() delete this->delegate_strategy; delete this->tableStrategyModel; delete this->detailViewerModel; + delete this->detailItemItemDelegate; delete this->roughStrategyViewerModel; + delete this->roughStrategyItemDelegate; delete this->timer; } +void StrategyExplorer::initializeView() { + // Clear previous state + ui->turnCardBox->clear(); + ui->riverCardBox->clear(); + this->cards.clear(); + + if (qSolverJob->analysis_mode == QSolverJob::AnalysisMode::HAND_ANALYSIS) { + // In hand analysis mode, we must get the card objects directly from the solver + // to ensure they are fully initialized with their correct deck index. + // Creating them from a string here would result in "detached" cards that + // cause errors when their deck index is requested. + shared_ptr solver = dynamic_pointer_cast(qSolverJob->get_solver()->get_solver()); + if (!solver) { + qDebug() << "Error: Could not get PCfrSolver instance in hand analysis mode."; + return; + } + + // This uses the new public getter added to PCfrSolver.h + const vector& solver_board_cards = solver->get_full_board_cards(); + + if (solver_board_cards.size() >= 4) { + const Card& turn_card = solver_board_cards[3]; + ui->turnCardBox->addItem(QString::fromStdString(turn_card.toFormattedString())); + ui->turnCardBox->setCurrentIndex(0); + ui->turnCardBox->setEnabled(false); + this->tableStrategyModel->setTrunCard(turn_card); + } + if (solver_board_cards.size() == 5) { + const Card& river_card = solver_board_cards[4]; + ui->riverCardBox->addItem(QString::fromStdString(river_card.toFormattedString())); + ui->riverCardBox->setCurrentIndex(0); + ui->riverCardBox->setEnabled(false); + this->tableStrategyModel->setRiverCard(river_card); + } + } else { + // In standard mode, populate with all possible cards. + Deck* deck = this->qSolverJob->get_solver()->get_deck(); + QString board_qstring = QString::fromStdString(this->qSolverJob->board); + + for(const auto& one_card : deck->getCards()){ + if(board_qstring.contains(QString::fromStdString(one_card.getCard()))) continue; + + this->cards.push_back(one_card); + QString card_str_formatted = QString::fromStdString(one_card.toFormattedString()); + this->ui->turnCardBox->addItem(card_str_formatted); + this->ui->riverCardBox->addItem(card_str_formatted); + + int new_index = this->ui->turnCardBox->count() - 1; + if(card_str_formatted.contains(QString::fromLocal8Bit("♦️")) || card_str_formatted.contains(QString::fromLocal8Bit("♥️️"))){ + this->ui->turnCardBox->setItemData(new_index, QBrush(Qt::red), Qt::ForegroundRole); + this->ui->riverCardBox->setItemData(new_index, QBrush(Qt::red), Qt::ForegroundRole); + } else { + this->ui->turnCardBox->setItemData(new_index, QBrush(Qt::black), Qt::ForegroundRole); + this->ui->riverCardBox->setItemData(new_index, QBrush(Qt::black), Qt::ForegroundRole); + } + } + + GameTreeNode::GameRound round = this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound(); + if (round == GameTreeNode::GameRound::FLOP) { + if (this->cards.size() > 0) { + this->tableStrategyModel->setTrunCard(this->cards[0]); + this->ui->turnCardBox->setCurrentIndex(0); + } + if (this->cards.size() > 1) { + this->tableStrategyModel->setRiverCard(this->cards[1]); + this->ui->riverCardBox->setCurrentIndex(1); + } + } else if (round == GameTreeNode::GameRound::TURN) { + this->ui->turnCardBox->setEnabled(false); + if (!this->cards.empty()) { + this->tableStrategyModel->setRiverCard(this->cards[0]); + this->ui->riverCardBox->setCurrentIndex(0); + } + } else if (round == GameTreeNode::GameRound::RIVER) { + this->ui->turnCardBox->setEnabled(false); + this->ui->riverCardBox->setEnabled(false); + } + } +} + void StrategyExplorer::item_expanded(const QModelIndex& index){ TreeItem *item = static_cast(index.internalPointer()); int num_child = item->childCount(); @@ -119,27 +179,23 @@ void StrategyExplorer::item_expanded(const QModelIndex& index){ } } -void StrategyExplorer::process_board(TreeItem* treeitem){ - vector board_str_arr = string_split(this->qSolverJob->board,','); - vector cards; - for(string one_board_str:board_str_arr){ - cards.push_back(Card(one_board_str)); - } - if(treeitem != NULL){ - if(treeitem->m_treedata.lock()->getRound() == GameTreeNode::GameRound::TURN && !this->tableStrategyModel->getTrunCard().empty()){ - cards.push_back(Card(this->tableStrategyModel->getTrunCard())); +void StrategyExplorer::process_board(shared_ptr node){ + vector local_cards = this->qSolverJob->get_solver()->get_solver()->get_initial_board_cards(); + if(node){ + if(node->getRound() == GameTreeNode::GameRound::TURN && !this->tableStrategyModel->getTrunCard().empty()){ + local_cards.push_back(this->tableStrategyModel->getTrunCard()); } - else if(treeitem->m_treedata.lock()->getRound() == GameTreeNode::GameRound::RIVER){ + else if(node->getRound() == GameTreeNode::GameRound::RIVER){ if(!this->tableStrategyModel->getTrunCard().empty()) - cards.push_back(Card(this->tableStrategyModel->getTrunCard())); + local_cards.push_back(this->tableStrategyModel->getTrunCard()); if(!this->tableStrategyModel->getRiverCard().empty()) - cards.push_back(Card(this->tableStrategyModel->getRiverCard())); + local_cards.push_back(this->tableStrategyModel->getRiverCard()); } } - this->ui->boardLabel->setText(QString("%1: ").arg(tr("board")) + Card::boardCards2html(cards)); + this->ui->boardLabel->setText(QString("%1: ").arg(tr("board")) + Card::boardCards2html(local_cards)); } -void StrategyExplorer::process_treeclick(TreeItem* treeitem){ +void StrategyExplorer::process_treeclick(const TreeItem* treeitem){ shared_ptr treenode = treeitem->m_treedata.lock(); if(treenode->getType() == GameTreeNode::GameTreeNodeType::ACTION){ shared_ptr actionnode = static_pointer_cast(treenode); @@ -163,9 +219,13 @@ void StrategyExplorer::process_treeclick(TreeItem* treeitem){ void StrategyExplorer::item_clicked(const QModelIndex& index){ try{ TreeItem * treeNode = static_cast(index.internalPointer()); + if (!treeNode) return; + shared_ptr gameNode = treeNode->m_treedata.lock(); + if (!gameNode) return; + this->process_treeclick(treeNode); - this->process_board(treeNode); - this->tableStrategyModel->setGameTreeNode(treeNode); + this->process_board(gameNode); + this->tableStrategyModel->setGameTreeNode(treeNode->m_treedata); this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->roughStrategyViewerModel->onchanged(); @@ -174,8 +234,7 @@ void StrategyExplorer::item_clicked(const QModelIndex& index){ } catch (const runtime_error& error) { - qDebug().noquote() << tr("Encountering error:");//.toStdString() << endl; - qDebug().noquote() << error.what() << "\n"; + qDebug().noquote() << tr("Encountering error:") << error.what(); } } @@ -185,13 +244,19 @@ void StrategyExplorer::selection_changed(const QItemSelection &selected, void StrategyExplorer::on_turnCardBox_currentIndexChanged(int index) { - if(this->cards.size() > 0 && index < this->cards.size()){ + if (this->qSolverJob->analysis_mode == QSolverJob::AnalysisMode::HAND_ANALYSIS) { + // In hand analysis mode, the card is fixed and set during initialization. + // The combo box is disabled, so this should not be triggered by the user. + // The existing handler logic is incorrect for this mode. + return; + } + if(index >= 0 && static_cast(index) < this->cards.size()){ this->tableStrategyModel->setTrunCard(this->cards[index]); this->tableStrategyModel->updateStrategyData(); // TODO this somehow cause bugs, crashes, why? //this->roughStrategyViewerModel->onchanged(); //this->ui->roughStrategyView->viewport()->update(); - this->process_board(this->tableStrategyModel->treeItem); + this->process_board(this->tableStrategyModel->getCurrentNode().lock()); } this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); @@ -199,32 +264,56 @@ void StrategyExplorer::on_turnCardBox_currentIndexChanged(int index) void StrategyExplorer::on_riverCardBox_currentIndexChanged(int index) { - if(this->cards.size() > 0 && index < this->cards.size()){ + if (this->qSolverJob->analysis_mode == QSolverJob::AnalysisMode::HAND_ANALYSIS) { + // In hand analysis mode, the card is fixed and set during initialization. + // The combo box is disabled, so this should not be triggered by the user. + // The existing handler logic is incorrect for this mode. + return; + } + if(index >= 0 && static_cast(index) < this->cards.size()){ this->tableStrategyModel->setRiverCard(this->cards[index]); this->tableStrategyModel->updateStrategyData(); //this->roughStrategyViewerModel->onchanged(); //this->ui->roughStrategyView->viewport()->update(); - this->process_board(this->tableStrategyModel->treeItem); + this->process_board(this->tableStrategyModel->getCurrentNode().lock()); } this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); } void StrategyExplorer::update_second(){ - if(this->cards.size() > 0){ - this->tableStrategyModel->updateStrategyData(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); - this->process_board(this->tableStrategyModel->treeItem); - } + // This timer is for auto-refreshing the view, for example if the solver is running in the background. + // A full data update is expensive, so we just trigger a repaint. + this->roughStrategyViewerModel->onchanged(); + this->ui->roughStrategyView->viewport()->update(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); } - void StrategyExplorer::onMouseMoveEvent(int i,int j){ this->detailWindowSetting.grid_i = i; this->detailWindowSetting.grid_j = j; + + // --- DEBUG START --- + if (i >= 0 && j >= 0) { + const auto& combos = this->tableStrategyModel->ui_strategy_table[i][j]; + qDebug() << "[DEBUG] onMouseMoveEvent for cell (" << i << "," << j << "). Found" << combos.size() << "combos."; + if (!combos.empty()) { + const auto& first_combo = combos[0]; + const auto& card_map = this->tableStrategyModel->cardint2card; + if (static_cast(first_combo.first) < card_map.size() && static_cast(first_combo.second) < card_map.size()) { + const Card& c1 = card_map[first_combo.first]; + const Card& c2 = card_map[first_combo.second]; + qDebug() << " - First combo:" << c1.toString().c_str() << c2.toString().c_str() + << "(ints:" << first_combo.first << "," << first_combo.second << ")"; + qDebug() << " - Card 1 empty:" << c1.empty() << ", Card 2 empty:" << c2.empty(); + } else { + qDebug() << " - First combo card ints out of range for card_map."; + } + } + } + // --- DEBUG END --- + this->ui->detailView->viewport()->update(); this->ui->strategyTableView->viewport()->update(); } @@ -232,6 +321,7 @@ void StrategyExplorer::onMouseMoveEvent(int i,int j){ void StrategyExplorer::on_strategyModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::STRATEGY; + this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); this->roughStrategyViewerModel->onchanged(); @@ -241,6 +331,7 @@ void StrategyExplorer::on_strategyModeButtom_clicked() void StrategyExplorer::on_ipRangeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::RANGE_IP; + this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); this->roughStrategyViewerModel->onchanged(); @@ -250,6 +341,7 @@ void StrategyExplorer::on_ipRangeButtom_clicked() void StrategyExplorer::on_oopRangeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::RANGE_OOP; + this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); this->roughStrategyViewerModel->onchanged(); @@ -259,14 +351,17 @@ void StrategyExplorer::on_oopRangeButtom_clicked() void StrategyExplorer::on_evModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::EV; + this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); + this->roughStrategyViewerModel->onchanged(); this->ui->roughStrategyView->viewport()->update(); } void StrategyExplorer::on_evOnlyModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::EV_ONLY; + this->tableStrategyModel->updateStrategyData(); this->ui->strategyTableView->viewport()->update(); this->ui->detailView->viewport()->update(); this->roughStrategyViewerModel->onchanged(); diff --git a/strategyexplorer.h b/strategyexplorer.h index 8d664765..1925f734 100644 --- a/strategyexplorer.h +++ b/strategyexplorer.h @@ -37,6 +37,7 @@ class StrategyExplorer : public QDialog ~StrategyExplorer(); private: + void initializeView(); DetailWindowSetting detailWindowSetting; QTimer *timer; Ui::StrategyExplorer *ui; @@ -48,8 +49,8 @@ class StrategyExplorer : public QDialog RoughStrategyViewerModel * roughStrategyViewerModel; RoughStrategyItemDelegate * roughStrategyItemDelegate; vector cards; - void process_treeclick(TreeItem* treeitem); - void process_board(TreeItem* treeitem); + void process_treeclick(const TreeItem* treeitem); + void process_board(shared_ptr node); public slots: void item_expanded(const QModelIndex& index); void item_clicked(const QModelIndex& index);