From 42b8940d1fcbfc7d32aff73c69dcf4526c4f28bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Mon, 1 Sep 2025 20:18:53 -0400 Subject: [PATCH 1/7] Make TexaSolver Build with CLion --- CMakeLists.txt | 253 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..dc66fbaf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,253 @@ +cmake_minimum_required(VERSION 3.14) + +project(TexasSolver LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_PREFIX_PATH "E:\\Qt\\6.9.2\\mingw_64\\") + +# 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 REQUIRED) + +# Find OpenMP for multi-threading support, matching the .pro file's configuration +find_package(OpenMP) + +# --- 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 From a385c5a9b24d8b878886dc0eeda48065d6431f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Tue, 2 Sep 2025 02:01:19 -0400 Subject: [PATCH 2/7] Fix default values in the mainwindow.ui and added CMakeLists.txt to allow TexaSolver Build with CLion --- CMakeLists.txt | 5 +---- mainwindow.ui | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dc66fbaf..94a5fd9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,19 +5,16 @@ project(TexasSolver LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_PREFIX_PATH "E:\\Qt\\6.9.2\\mingw_64\\") - # 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 REQUIRED) +find_package(Qt5 COMPONENTS Core Gui Widgets LinguistTools REQUIRED) # Find OpenMP for multi-threading support, matching the .pro file's configuration find_package(OpenMP) - # --- Source File Definitions --- # Core library sources (shared between GUI and CLI) diff --git a/mainwindow.ui b/mainwindow.ui index 00570485..5d356479 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> @@ -139,9 +139,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 +715,7 @@ p, li { white-space: pre-wrap; } - 3 + 2 @@ -729,7 +729,7 @@ p, li { white-space: pre-wrap; } - 50 + @@ -743,7 +743,7 @@ p, li { white-space: pre-wrap; } - 200 + 100 @@ -1113,7 +1113,7 @@ p, li { white-space: pre-wrap; } 0 0 1134 - 26 + 21 From ee7e9edc98c1969a434b908b45d066cb36b2f4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Tue, 2 Sep 2025 02:36:04 -0400 Subject: [PATCH 3/7] Fixed OpenMP 3.0 support --- CMakeLists.txt | 3 ++- src/compairer/Dic5Compairer.cpp | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 94a5fd9b..b7ed9411 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ 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) @@ -14,7 +15,7 @@ set(CMAKE_AUTORCC ON) find_package(Qt5 COMPONENTS Core Gui Widgets LinguistTools REQUIRED) # Find OpenMP for multi-threading support, matching the .pro file's configuration -find_package(OpenMP) +find_package(OpenMP REQUIRED) # --- Source File Definitions --- # Core library sources (shared between GUI and CLI) diff --git a/src/compairer/Dic5Compairer.cpp b/src/compairer/Dic5Compairer.cpp index 8410ea8e..798dd259 100644 --- a/src/compairer/Dic5Compairer.cpp +++ b/src/compairer/Dic5Compairer.cpp @@ -9,7 +9,6 @@ #include #include #include "time.h" -#include "unistd.h" #define SUIT_0_MASK 0x1111111111111 #define SUIT_1_MASK 0x2222222222222 @@ -287,4 +286,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)); } - From 3f3f3c2ad7f55b94d3e7c298053733f0a270227e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Sat, 6 Sep 2025 14:23:51 -0400 Subject: [PATCH 4/7] Implement node locking to analyze games - first commit --- include/Card.h | 8 +- include/nodes/GameActions.h | 8 +- include/runtime/PokerSolver.h | 6 +- include/runtime/qsolverjob.h | 7 +- include/solver/PCfrSolver.h | 20 +- include/solver/Solver.h | 24 +- include/solver/solver_options.h | 52 ++++ include/tools/CommandLineTool.h | 2 +- include/ui/tablestrategymodel.h | 8 +- include/ui/treeitem.h | 3 + main.cpp | 34 ++- mainwindow.cpp | 127 ++++++++-- mainwindow.ui | 17 ++ src/Card.cpp | 16 +- src/nodes/GameActions.cpp | 11 +- src/runtime/PokerSolver.cpp | 7 +- src/runtime/qsolverjob.cpp | 12 +- src/solver/PCfrSolver.cpp | 350 ++++++++++++++++++--------- src/ui/detailitemdelegate.cpp | 70 +++--- src/ui/detailviewermodel.cpp | 4 + src/ui/roughstrategyitemdelegate.cpp | 20 +- src/ui/roughstrategyviewermodel.cpp | 13 +- src/ui/tablestrategymodel.cpp | 229 +++++++++--------- src/ui/treeitem.cpp | 31 +++ strategyexplorer.cpp | 244 ++++++++++++------- strategyexplorer.h | 51 ++-- 26 files changed, 907 insertions(+), 467 deletions(-) create mode 100644 include/solver/solver_options.h diff --git a/include/Card.h b/include/Card.h index 7b525852..513c1cc8 100644 --- a/include/Card.h +++ b/include/Card.h @@ -21,9 +21,9 @@ class Card { explicit Card(string card,int card_number_in_deck); Card(string card); string getCard(); - int getCardInt(); + int getCardInt() const; bool empty(); - int getNumberInDeckInt(); + int getNumberInDeckInt() const; static int card2int(Card card); static int strCard2int(string card); static string intCard2Str(int card); @@ -43,8 +43,8 @@ class Card { static int rankToInt(char rank); static int suitToInt(char suit); static vector getSuits(); - string toString(); - string toFormattedString(); + string toString() const; + string toFormattedString() const; QString toFormattedHtml(); }; 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/runtime/PokerSolver.h b/include/runtime/PokerSolver.h index 705919bc..3973a3e6 100644 --- a/include/runtime/PokerSolver.h +++ b/include/runtime/PokerSolver.h @@ -44,13 +44,15 @@ class PokerSolver { float accuracy, bool use_isomorphism, int use_halffloats, - int threads + int threads, + const vector& locked_nodes, + const std::optional& full_board_situation ); void stop(); long long estimate_tree_memory(QString range1,QString range2,QString board); vector player1Range; vector player2Range; - void dump_strategy(QString dump_file,int dump_rounds); + void dump_strategy(QString dump_file,int dump_rounds = 2); shared_ptr get_game_tree(){return this->game_tree;}; Deck* get_deck(){return &this->deck;} shared_ptr get_solver(){return this->solver;} diff --git a/include/runtime/qsolverjob.h b/include/runtime/qsolverjob.h index 507bb05f..39f64d14 100644 --- a/include/runtime/qsolverjob.h +++ b/include/runtime/qsolverjob.h @@ -7,6 +7,8 @@ #include #include #include "qstextedit.h" +#include "include/solver/solver_options.h" +#include #include #include @@ -39,7 +41,7 @@ class QSolverJob : public QThread float small_blind=0.5; float big_blind=1; float stack=20 + 5; - float allin_threshold = 0.67; + float allin_threshold = 0.67f; string range_ip; string range_oop; string board; @@ -49,6 +51,9 @@ class QSolverJob : public QThread int use_halffloats=0; int print_interval=10; int dump_rounds = 2; + // Add these new members for analysis features + std::vector locked_nodes; + std::optional full_board_situation; shared_ptr gtbs; PokerSolver* get_solver(); diff --git a/include/solver/PCfrSolver.h b/include/solver/PCfrSolver.h index cce4f68c..1b1f31c5 100644 --- a/include/solver/PCfrSolver.h +++ b/include/solver/PCfrSolver.h @@ -93,12 +93,16 @@ class PCfrSolver:public Solver { int num_threads ); ~PCfrSolver(); - void train() override; + void train(const vector& locked_nodes, const std::optional& full_board) override; void stop() override; json dumps(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_strategy(shared_ptr node,vector chance_cards, const std::string& path) override; + vector>> get_evs(shared_ptr node,vector chance_cards, const std::string& path) override; private: + // New members for analysis features + map m_locked_nodes_map; + std::optional m_full_board_situation; + vector> ranges; vector range1; vector range2; @@ -134,12 +138,12 @@ class PCfrSolver:public Solver { vector> getReachProbs(); static vector noDuplicateRange(const vector& private_range,uint64_t board_long); void setTrainable(shared_ptr root); - vector cfr(int player, shared_ptr node, const vector& reach_probs, int iter, uint64_t current_board,int deal); + vector cfr(int player, shared_ptr node, const vector& reach_probs, int iter, uint64_t current_board,int deal, const string& path); vector getAllAbstractionDeal(int deal); - vector chanceUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_boardi,int deal); - vector showdownUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_board,int deal); - vector actionUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_board,int deal); - vector terminalUtility(int player,shared_ptr node,const vector& reach_prob,int iter,uint64_t current_board,int deal); + vector chanceUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_board,int deal, const string& path); + vector showdownUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_board,int deal, const string& path); + vector actionUtility(int player,shared_ptr node,const vector& reach_probs,int iter,uint64_t current_board,int deal, const string& path); + vector terminalUtility(int player,shared_ptr node,const vector& reach_prob,int iter,uint64_t current_board,int deal, const string& path); void findGameSpecificIsomorphisms(); void purnTree(); void exchangeRange(json& strategy,int rank1,int rank2,shared_ptr one_node); diff --git a/include/solver/Solver.h b/include/solver/Solver.h index d7f7271b..9dcec0b4 100644 --- a/include/solver/Solver.h +++ b/include/solver/Solver.h @@ -7,6 +7,8 @@ #include +#include +#include "solver_options.h" // For LockedNode class Solver { public: @@ -14,14 +16,30 @@ class Solver { NONE, PUBLIC }; + /** + * @brief For analyzing specific board runouts, you should first construct a + * specialized GameTree. This tree will be much smaller as it won't have + * chance nodes for the board cards. Then, pass this pruned tree to the + * solver's constructor. + * + * @brief For node locking, pass a vector of LockedNode objects to the train() + * method to force specific strategies at certain game nodes. + */ Solver(); Solver(shared_ptr tree); shared_ptr getTree(); - virtual void train() = 0; + + // Default train methods that delegate to the full version. + virtual void train() { train({}, std::nullopt); } + virtual void train(const vector& locked_nodes) { train(locked_nodes, std::nullopt); } + // Train with node locking and/or a specific full board runout. + // This must be implemented by concrete solver classes. + virtual void train(const vector& locked_nodes, const std::optional& full_board) = 0; + virtual void stop() = 0; virtual json dumps(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_strategy(shared_ptr node,vector cards, const std::string& path) = 0; + virtual vector>> get_evs(shared_ptr node,vector cards, const std::string& path) = 0; shared_ptr tree; }; diff --git a/include/solver/solver_options.h b/include/solver/solver_options.h new file mode 100644 index 00000000..a3529f1e --- /dev/null +++ b/include/solver/solver_options.h @@ -0,0 +1,52 @@ +// +// Created by Christian Foernges +// + +#ifndef TEXASSOLVER_SOLVER_OPTIONS_H +#define TEXASSOLVER_SOLVER_OPTIONS_H + +#include +#include +#include + +// Forward-declare Card, as its definition is in another header (e.g., GameTree.h). +// It's typically an integer or a small struct. +class Card; + +// An Action is represented by an integer. +// e.g., -1=Fold, 0=Call/Check, >0 = Raise amount +using Action = int; + +// A strategy is a probability distribution over possible actions. +using Strategy = std::map; + +/** + * @brief Defines a locked strategy for a specific player at a specific game node. + * This forces the solver to use a fixed strategy at this point, allowing analysis + * of non-standard lines. + */ +struct LockedNode { + // A unique string identifying the node, built from the sequence of actions. + // Example: "r100/c/" for a preflop raise to 100 followed by a call. + std::string node_path; + + // The player index (0 for OOP, 1 for IP) whose strategy is to be locked. + int player_to_lock; + + // The fixed strategy to use at this node. The probabilities must sum to 1.0. + Strategy locked_strategy; +}; + +/** + * @brief Defines a specific hand scenario with a full 5-card board. + * When a solver is analyzing a game with this option, it will prune the + * game tree traversal to only this specific board runout, dramatically + * increasing performance. + */ +struct FullBoardSituation { + // The 5 community cards (flop, turn, and river), represented by their integer IDs. + // Must have a size of 5. + std::vector board_cards; +}; + +#endif //TEXASSOLVER_SOLVER_OPTIONS_H \ No newline at end of file diff --git a/include/tools/CommandLineTool.h b/include/tools/CommandLineTool.h index 3b078fa9..5c8e31a7 100644 --- a/include/tools/CommandLineTool.h +++ b/include/tools/CommandLineTool.h @@ -33,7 +33,7 @@ class CommandLineTool{ float small_blind=0.5; float big_blind=1; float stack=20 + 5; - float allin_threshold = 0.67; + float allin_threshold = 0.67f; string range_ip; string range_oop; string board; diff --git a/include/ui/tablestrategymodel.h b/include/ui/tablestrategymodel.h index ede362a2..7225897a 100644 --- a/include/ui/tablestrategymodel.h +++ b/include/ui/tablestrategymodel.h @@ -29,10 +29,10 @@ class TableStrategyModel : public QAbstractItemModel 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 setTurnCard(Card turn_card); void setRiverCard(Card river_card); - Card getTrunCard(){return turn_card;}; - Card getRiverCard(){return river_card;}; + Card getTurnCard() const; + Card getRiverCard() const; void updateStrategyData(); const vector> get_strategy(int i,int j) const; const vector>> get_total_strategy() const; @@ -53,7 +53,7 @@ class TableStrategyModel : public QAbstractItemModel private: QSolverJob* qSolverJob; - void setupModelData(); + void resetDynamicData(); Card turn_card; Card river_card; diff --git a/include/ui/treeitem.h b/include/ui/treeitem.h index 33c54f20..fb8af71e 100644 --- a/include/ui/treeitem.h +++ b/include/ui/treeitem.h @@ -4,6 +4,7 @@ #include #include #include "include/nodes/GameTreeNode.h" +#include #include "include/nodes/ActionNode.h" #include "include/nodes/ChanceNode.h" #include "include/nodes/TerminalNode.h" @@ -29,6 +30,8 @@ class TreeItem int row() const; TreeItem *parentItem() { return m_parentItem; } bool removeChild(int row); + std::string getActionPath() const; + private: QList m_childItems; diff --git a/main.cpp b/main.cpp index 1fb0ac1f..2745d9b4 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(); } } @@ -45,28 +41,28 @@ int main(int argc, char *argv[]) QSettings setting("TexasSolver", "Setting"); setting.beginGroup("solver"); QString language_str = setting.value("language").toString(); - QTranslator trans; - if(language_str == ""){ + if(language_str.isEmpty()){ QStringList languages; - languages << "English" << QString::fromLocal8Bit("简体中文"); + languages << "English" << QString::fromUtf8("简体中文"); QString lang = QInputDialog::getItem(NULL,"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"); + }else if(lang == QString::fromUtf8("简体中文")){ 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"); - } + } + + QTranslator trans; + if(language_str == "EN"){ + trans.load(":/lang_en.qm"); + }else if(language_str == "CN"){ + trans.load(":/lang_cn.qm"); + } + + if (!language_str.isEmpty()) { a.installTranslator(&trans); } diff --git a/mainwindow.cpp b/mainwindow.cpp index ec81aa4b..723026ef 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -3,7 +3,9 @@ #include "stdio.h" #include "include/runtime/qsolverjob.h" #include +#include #include "include/library.h" +#include "include/solver/solver_options.h" QSTextEdit* MainWindow::s_textEdit = 0; @@ -71,13 +73,16 @@ QSTextEdit * MainWindow::get_logwindow(){ MainWindow::~MainWindow() { + // qSolverJob is a QThread and was created without a parent, + // so we must manage its lifecycle manually. delete qSolverJob; - delete qFileSystemModel; - delete ip_delegate; - delete ip_model; - delete oop_delegate; - delete oop_model; + + // The ui pointer is the only other one we need to manually delete. delete ui; + + // All other QObject-derived members (qFileSystemModel, delegates, models) + // were created with `this` as their parent, so Qt will handle + // their deletion automatically. } void MainWindow::on_actionjson_triggered(){ @@ -412,6 +417,84 @@ void MainWindow::on_ip_range(QString range_text){ void MainWindow::on_buttomSolve_clicked() { + // --- Analysis Features --- + + // 1. Parse Node Locking rules from the UI + std::vector locked_nodes; + std::map, LockedNode> temp_locked_nodes_map; + + QString node_locking_text = ui->nodeLockingText->toPlainText(); + QStringList lines = node_locking_text.split('\n', Qt::SkipEmptyParts); + + for (const QString& line : lines) { + QString trimmed_line = line.trimmed(); + if (trimmed_line.startsWith('#') || trimmed_line.isEmpty()) { + continue; // Skip comments and empty lines + } + + QStringList parts = trimmed_line.split(';', Qt::SkipEmptyParts); + if (parts.size() != 4) { + qDebug().noquote() << tr("Warning: Skipping invalid node lock rule (wrong format): ") << line; + continue; + } + + std::string path = parts[0].trimmed().toStdString(); + int player = parts[1].trimmed().toInt(); + QString action_str = parts[2].trimmed().toLower(); + double prob = parts[3].trimmed().toDouble() / 100.0; // Convert from percentage + + Action action_key; + if (action_str == "f" || action_str == "fold") { + action_key = -1; + } else if (action_str == "c" || action_str == "check" || action_str == "call") { + action_key = 0; + } else if (action_str.startsWith("b_") || action_str.startsWith("bet_") || action_str.startsWith("bet ")) { + action_key = static_cast(action_str.split(QRegularExpression("[_ ]")).last().toFloat()); + } else if (action_str.startsWith("r_") || action_str.startsWith("raise_") || action_str.startsWith("raise ")) { + action_key = static_cast(action_str.split(QRegularExpression("[_ ]")).last().toFloat()); + } else { + qDebug().noquote() << tr("Warning: Skipping invalid action in node lock rule: ") << action_str; + continue; + } + + auto map_key = std::make_pair(path, player); + temp_locked_nodes_map[map_key].node_path = path; + temp_locked_nodes_map[map_key].player_to_lock = player; + temp_locked_nodes_map[map_key].locked_strategy[action_key] = prob; + } + + // Convert map to final vector and assign to the job + for (auto const& [key, val] : temp_locked_nodes_map) { + locked_nodes.push_back(val); + } + qSolverJob->locked_nodes = locked_nodes; + + // 2. Check for Full Board Analysis + std::optional full_board_situation = std::nullopt; + if (ui->fullBoardAnalysisCheck->isChecked()) { + QString board_text = ui->boardText->toPlainText(); + vector board_str_arr = string_split(board_text.toStdString(), ','); + + if (board_str_arr.size() == 5) { + FullBoardSituation situation; + for (const auto& card_str : board_str_arr) { + if (!card_str.empty()) { + situation.board_cards.push_back(Card::strCard2int(card_str)); + } + } + if (situation.board_cards.size() == 5) { + full_board_situation = situation; + qDebug().noquote() << tr("Full board analysis mode activated for board: ") << board_text; + } else { + qDebug().noquote() << tr("Warning: Full board analysis enabled, but board does not contain 5 valid cards. Solving normally."); + } + } else { + qDebug().noquote() << tr("Warning: Full board analysis enabled, but board does not contain 5 cards. Solving normally."); + } + } + qSolverJob->full_board_situation = full_board_situation; + + // --- Set Standard Solver Options --- qSolverJob->max_iteration = ui->iterationText->text().toInt(); qSolverJob->accuracy = ui->exploitabilityText->text().toFloat(); qSolverJob->print_interval = ui->logIntervalText->text().toInt(); @@ -453,18 +536,28 @@ 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; - }else{ - this->ui->logOutput->log_with_signal(QString::fromStdString(tfm::format("Error : board %s not recognized",qSolverJob->board))); - return; + + QString full_board_text = this->ui->boardText->toPlainText(); + vector board_str_arr = string_split(full_board_text.toStdString(), ','); + + if (ui->fullBoardAnalysisCheck->isChecked() && board_str_arr.size() >= 3) { + // In analysis mode, we build the tree from the flop, even if the full board is provided. + // The solver will use the full board to prune, but the tree structure starts at the flop. + qSolverJob->board = QString::fromStdString(board_str_arr[0] + "," + board_str_arr[1] + "," + board_str_arr[2]).toStdString(); + qSolverJob->current_round = 1; // Force flop start + } else { + // Original logic for building trees from different streets + qSolverJob->board = full_board_text.toStdString(); + 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; diff --git a/mainwindow.ui b/mainwindow.ui index 5d356479..a2e5ef97 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -882,6 +882,23 @@ p, li { white-space: pre-wrap; } Solver Options + + + + Full Board Analysis + + + + + + + Node Locking Rules (path; player; action; probability%): + + + + + + diff --git a/src/Card.cpp b/src/Card.cpp index ca91ac19..da1d622f 100644 --- a/src/Card.cpp +++ b/src/Card.cpp @@ -26,11 +26,11 @@ string Card::getCard() { 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; } @@ -217,11 +217,11 @@ 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", "♦️"); @@ -233,12 +233,12 @@ string Card::toFormattedString() { QString Card::toFormattedHtml() { QString qString = QString::fromStdString(this->card); if(qString.contains("c")) - qString = qString.replace("c", QString::fromLocal8Bit("♣<\/span>")); + qString = qString.replace("c", QString::fromLocal8Bit("")); else if(qString.contains("d")) - qString = qString.replace("d", QString::fromLocal8Bit("♦<\/span>")); + qString = qString.replace("d", QString::fromLocal8Bit("")); else if(qString.contains("h")) - qString = qString.replace("h", QString::fromLocal8Bit("♥<\/span>")); + qString = qString.replace("h", QString::fromLocal8Bit("")); else if(qString.contains("s")) - qString = qString.replace("s", QString::fromLocal8Bit("♠<\/span>")); + qString = qString.replace("s", QString::fromLocal8Bit("")); return qString; } diff --git a/src/nodes/GameActions.cpp b/src/nodes/GameActions.cpp index 920a9be7..cf9eb475 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,10 +40,11 @@ string GameActions::pokerActionToString(GameTreeNode::PokerActions pokerActions) } } -string GameActions::toString() { +string GameActions::toString() const { if(this->amount == -1) { return this->pokerActionToString(this->action); }else{ - return this->pokerActionToString(this->action) + " " + to_string(amount); + // Use tinyformat for consistent float formatting, %g removes trailing zeros. + return tfm::format("%s %g", this->pokerActionToString(this->action), this->amount); } } diff --git a/src/runtime/PokerSolver.cpp b/src/runtime/PokerSolver.cpp index bff1619c..bff35b2b 100644 --- a/src/runtime/PokerSolver.cpp +++ b/src/runtime/PokerSolver.cpp @@ -84,12 +84,13 @@ 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) { + int print_interval, string algorithm,int warmup,float accuracy,bool use_isomorphism, int use_halffloats, int threads, + const vector& locked_nodes, const std::optional& full_board) { string player1RangeStr = p1_range; string player2RangeStr = p2_range; @@ -126,7 +127,7 @@ void PokerSolver::train(string p1_range, string p2_range, string boards, string , use_halffloats , threads ); - this->solver->train(); + this->solver->train(locked_nodes, full_board); } void PokerSolver::dump_strategy(QString dump_file,int dump_rounds) { diff --git a/src/runtime/qsolverjob.cpp b/src/runtime/qsolverjob.cpp index 27a663f8..11d26c4d 100644 --- a/src/runtime/qsolverjob.cpp +++ b/src/runtime/qsolverjob.cpp @@ -96,7 +96,9 @@ void QSolverJob::stop(){ void QSolverJob::solving(){ // TODO 为什么ui上多次求解会积累memory?哪里leak了? - // TODO 为什么有时候会莫名闪退? + // TODO 为什么有时候会莫名闪退? + // NOTE: This requires modifying the PokerSolver::train method to accept + // the new analysis parameters (locked_nodes and full_board_situation). qDebug().noquote() << tr("Start Solving..");//.toStdString() << std::endl; if(this->mode == Mode::HOLDEM){ @@ -112,7 +114,9 @@ void QSolverJob::solving(){ this->accuracy, this->use_isomorphism, this->use_halffloats, - this->thread_number + this->thread_number, + this->locked_nodes, + this->full_board_situation ); }else if(this->mode == Mode::SHORTDECK){ this->ps_shortdeck.train( @@ -127,7 +131,9 @@ void QSolverJob::solving(){ this->accuracy, this->use_isomorphism, this->use_halffloats, - this->thread_number + this->thread_number, + this->locked_nodes, + this->full_board_situation ); } qDebug().noquote() << tr("Solving done.");//.toStdString() << std::endl; diff --git a/src/solver/PCfrSolver.cpp b/src/solver/PCfrSolver.cpp index 29395eda..99339ac8 100644 --- a/src/solver/PCfrSolver.cpp +++ b/src/solver/PCfrSolver.cpp @@ -2,6 +2,7 @@ // Created by Xuefeng Huang on 2020/1/31. // +#include #include #include "include/solver/PCfrSolver.h" #include @@ -168,10 +169,9 @@ vector PCfrSolver::getAllAbstractionDeal(int deal){ 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); } @@ -184,15 +184,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; @@ -206,20 +203,20 @@ vector PCfrSolver::getAllAbstractionDeal(int deal){ } vector PCfrSolver::cfr(int player, shared_ptr node, const vector &reach_probs, int iter, - uint64_t current_board,int deal) { + uint64_t current_board,int deal, const string& path) { switch(node->getType()) { case GameTreeNode::ACTION: { shared_ptr action_node = std::dynamic_pointer_cast(node); - return actionUtility(player, action_node, reach_probs, iter, current_board,deal); + return actionUtility(player, action_node, reach_probs, iter, current_board,deal, path); }case GameTreeNode::SHOWDOWN: { shared_ptr showdown_node = std::dynamic_pointer_cast(node); - return showdownUtility(player, showdown_node, reach_probs, iter, current_board,deal); + return showdownUtility(player, showdown_node, reach_probs, iter, current_board,deal, path); }case GameTreeNode::TERMINAL: { shared_ptr terminal_node = std::dynamic_pointer_cast(node); - return terminalUtility(player, terminal_node, reach_probs, iter, current_board,deal); + return terminalUtility(player, terminal_node, reach_probs, iter, current_board,deal, path); }case GameTreeNode::CHANCE: { shared_ptr chance_node = std::dynamic_pointer_cast(node); - return chanceUtility(player, chance_node, reach_probs, iter, current_board,deal); + return chanceUtility(player, chance_node, reach_probs, iter, current_board,deal, path); }default: throw runtime_error("node type unknown"); } @@ -227,14 +224,14 @@ 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) { + uint64_t current_board,int deal, const string& path) { vector& cards = this->deck.getCards(); //float[] cardWeights = getCardsWeights(player,reach_probs[1 - player],current_board); - int card_num = node->getCards().size(); + size_t card_num = node->getCards().size(); if(card_num % 4 != 0) throw runtime_error("card num cannot round 4"); // 可能的发牌情况,2代表每个人的holecard是两张 - int possible_deals = node->getCards().size() - Card::long2board(current_board).size() - 2; + size_t possible_deals = node->getCards().size() - Card::long2board(current_board).size() - 2; int oppo = 1 - player; //vector chance_utility(reach_probs[player].size()); @@ -265,17 +262,13 @@ 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()); + const Card& one_card = node->getCards()[i_card]; + uint64_t card_long = Card::boardInt2long(one_card.getCardInt()); + if (i == cardr) { 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; } @@ -288,22 +281,61 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< vector valid_cards; valid_cards.reserve(node->getCards().size()); - 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}); - 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; - valid_cards.push_back(card); + if (m_full_board_situation.has_value()) { + // Full board analysis: only "deal" the specific card for this street. + const auto& full_board_cards = m_full_board_situation->board_cards; + vector current_board_vec = Card::long2board(current_board); + size_t num_cards_on_board = current_board_vec.size(); + int card_to_deal_int = -1; + + // This logic assumes the solver deals one card at a time and that the tree + // has sequential chance nodes for multi-card streets like the flop. + if (node->getRound() == GameTreeNode::GameRound::FLOP && full_board_cards.size() >= 3) { + size_t num_dealt_on_street = num_cards_on_board - this->initial_board.size(); + if (iter == 0) { + qDebug().noquote() << "FLOP analysis: num_cards_on_board=" << num_cards_on_board << "initial_board.size()=" << this->initial_board.size() << "num_dealt_on_street=" << num_dealt_on_street; + } + if (num_dealt_on_street < 3) { + card_to_deal_int = full_board_cards[num_dealt_on_street]; + } + } else if (node->getRound() == GameTreeNode::GameRound::TURN && full_board_cards.size() >= 4) { + card_to_deal_int = full_board_cards[3]; + } else if (node->getRound() == GameTreeNode::GameRound::RIVER && full_board_cards.size() >= 5) { + card_to_deal_int = full_board_cards[4]; + } + + if (card_to_deal_int != -1) { + if (iter == 0) { // Log only on first iteration + qDebug().noquote() << "Pruning chance node at round" << node->getRound() << ". Dealing only card:" << Card::intCard2Str(card_to_deal_int).c_str(); + } + for (size_t i = 0; i < node->getCards().size(); ++i) { + if (node->getCards()[i].getCardInt() == card_to_deal_int) { + uint64_t card_long = Card::boardInt2long(card_to_deal_int); + if (!Card::boardsHasIntercept(card_long, current_board)) { + valid_cards.push_back(static_cast(i)); + } + break; // Found our specific card + } + } + } + } else { // Not in full board mode, use original logic. + for(std::size_t card = 0;card < node->getCards().size();card ++) { + shared_ptr one_child = node->getChildren(); + 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; + valid_cards.push_back(static_cast(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]; + int card_idx = valid_cards[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_idx]; + 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) { @@ -325,7 +357,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< if (oppoPrivateCards.size() != this->ranges[1 - player].size()) throw runtime_error("length not match"); #endif - int player_hand_len = this->ranges[oppo].size(); + size_t player_hand_len = this->ranges[oppo].size(); for (int player_hand = 0; player_hand < player_hand_len; player_hand++) { PrivateCards &one_private = this->ranges[oppo][player_hand]; uint64_t privateBoardLong = one_private.toBoardLong(); @@ -342,57 +374,70 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< int new_deal; if(deal == 0){ - new_deal = card + 1; + 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"); + if(origin_deal == card_idx) 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{ 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; + } else { + vector child_utility = this->cfr(player, one_child, new_reach_probs, iter, new_board_long, new_deal, path); + 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])); - vector child_utility; - int offset = this->color_iso_offset[deal][one_card->getCardInt() % 4]; - if(offset < 0) { - 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]; - exchange_color(child_utility,this->pcm.getPreflopCards(player),rank1,rank2); - }else{ - child_utility = results[one_card->getNumberInDeckInt()]; + + if (m_full_board_situation.has_value()) { + // When pruning, there's no isomorphism, and only one valid card. + if (!valid_cards.empty()) { + int card_idx = valid_cards[0]; + const Card& one_card = node->getCards()[card_idx]; + vector& child_utility = results[one_card.getNumberInDeckInt()]; + if (!child_utility.empty()) { + // No multiplier during pruning, as it's not a statistical sample. + chance_utility = child_utility; + } } - if(child_utility.empty()) - continue; + } else { + // Original logic with isomorphism and multipliers + for(std::size_t card = 0;card < node->getCards().size();card ++) { + const Card& one_card = node->getCards()[card]; + vector child_utility; + int offset = this->color_iso_offset[deal][one_card.getCardInt() % 4]; + if(offset < 0) { + 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]; + exchange_color(child_utility,this->pcm.getPreflopCards(player),rank1,rank2); + }else{ + child_utility = results[one_card.getNumberInDeckInt()]; + } + if(child_utility.empty()) + continue; -#ifdef DEBUG - if(child_utility.size() != chance_utility.size()) throw runtime_error("length not match"); -#endif - if(iter > this->warmup) { - for (std::size_t i = 0; i < child_utility.size(); i++) - chance_utility[i] += child_utility[i]; - }else{ - for (std::size_t i = 0; i < child_utility.size(); i++) - chance_utility[i] += child_utility[i] * multiplier[card]; + #ifdef DEBUG + if(child_utility.size() != chance_utility.size()) throw runtime_error("length not match"); + #endif + if(iter > this->warmup) { + for (std::size_t i = 0; i < child_utility.size(); i++) + chance_utility[i] += child_utility[i]; + }else{ + for (std::size_t i = 0; i < child_utility.size(); i++) + chance_utility[i] += child_utility[i] * multiplier[card]; + } } } - #ifdef DEBUG if(this->monteCarolAlg == MonteCarolAlg::PUBLIC) { throw runtime_error("not possible"); @@ -406,7 +451,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< vector PCfrSolver::actionUtility(int player, shared_ptr node, const vector &reach_probs, int iter, - uint64_t current_board,int deal) { + uint64_t current_board,int deal, const string& path) { int oppo = 1 - player; const vector& node_player_private_cards = this->ranges[node->getPlayer()]; @@ -415,25 +460,56 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< vector>& children = node->getChildrens(); vector& actions = node->getActions(); - shared_ptr trainable; + vector current_strategy; + shared_ptr trainable = nullptr; + bool is_locked = false; + int node_player = node->getPlayer(); - /* - if(iter <= this->warmup){ - vector deals = this->getAllAbstractionDeal(deal); - trainable = node->getTrainable(deals[0]); - }else{ - trainable = node->getTrainable(deal); - } - */ - trainable = node->getTrainable(deal,true,this->use_halffloats); + auto it = m_locked_nodes_map.find(path); + if (it != m_locked_nodes_map.end() && it->second->player_to_lock == node_player) { + is_locked = true; + if (iter == 0) { // Log only on the first iteration to avoid spam + qDebug().noquote() << "Applying locked strategy at path:" << QString::fromStdString(path); + } + const Strategy& locked_strategy = it->second->locked_strategy; + current_strategy.assign(actions.size() * node_player_private_cards.size(), 0.0f); + + for (size_t action_id = 0; action_id < actions.size(); ++action_id) { + GameActions& game_action = actions[action_id]; + Action locked_action_key; + switch(game_action.getAction()) { + case GameTreeNode::PokerActions::FOLD: locked_action_key = -1; break; + case GameTreeNode::PokerActions::CHECK: locked_action_key = 0; break; + case GameTreeNode::PokerActions::CALL: locked_action_key = 0; break; + case GameTreeNode::PokerActions::BET: locked_action_key = static_cast(game_action.getAmount()); break; + case GameTreeNode::PokerActions::RAISE: locked_action_key = static_cast(game_action.getAmount()); break; + default: throw runtime_error("Unknown action type for locking"); + } -#ifdef DEBUG - if(trainable == nullptr){ - throw runtime_error("null trainable"); + auto strat_it = locked_strategy.find(locked_action_key); + if (strat_it != locked_strategy.end()) { + double prob = strat_it->second; + for (size_t hand_id = 0; hand_id < node_player_private_cards.size(); ++hand_id) { + current_strategy[hand_id + action_id * node_player_private_cards.size()] = prob; + } + } + } + } else { + /* + if(iter <= this->warmup){ + vector deals = this->getAllAbstractionDeal(deal); + trainable = node->getTrainable(deals[0]); + }else{ + trainable = node->getTrainable(deal); + } + */ + trainable = node->getTrainable(deal,true,this->use_halffloats); + #ifdef DEBUG + if(trainable == nullptr) throw runtime_error("null trainable"); + #endif + current_strategy = trainable->getcurrentStrategy(); } -#endif - const vector current_strategy = trainable->getcurrentStrategy(); #ifdef DEBUG if (current_strategy.size() != actions.size() * node_player_private_cards.size()) { node->printHistory(); @@ -451,10 +527,10 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< vector regrets(actions.size() * node_player_private_cards.size()); vector> all_action_utility(actions.size()); - int node_player = node->getPlayer(); vector> results(actions.size()); for (std::size_t action_id = 0; action_id < actions.size(); action_id++) { + string next_path = path + actions[action_id].toString() + "/"; if (node_player != player) { vector new_reach_prob = vector(reach_probs.size()); @@ -463,12 +539,10 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< new_reach_prob[hand_id] = reach_probs[hand_id] * strategy_prob; } //#pragma omp task shared(results,action_id) - results[action_id] = this->cfr(player, children[action_id], new_reach_prob, iter, - current_board,deal); + results[action_id] = this->cfr(player, children[action_id], new_reach_prob, iter, current_board, deal, next_path); }else { //#pragma omp task shared(results,action_id) - results[action_id] = this->cfr(player, children[action_id], reach_probs, iter, - current_board,deal); + results[action_id] = this->cfr(player, children[action_id], reach_probs, iter, current_board, deal, next_path); } } @@ -519,7 +593,7 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< } } - if(!this->distributing_task && !this->collecting_statics) { + if(!this->distributing_task && !this->collecting_statics && !is_locked) { if (iter > this->warmup) { trainable->updateRegrets(regrets, iter + 1, reach_probs); }/*else if(iter < this->warmup){ @@ -529,7 +603,12 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< }*/ else { // iter == this->warmup - vector deals = this->getAllAbstractionDeal(deal); + vector deals; + if (m_full_board_situation.has_value()) { + deals.push_back(deal); + } else { + deals = this->getAllAbstractionDeal(deal); + } shared_ptr standard_trainable = nullptr; for (int one_deal : deals) { shared_ptr one_trainable = node->getTrainable(one_deal,true,this->use_halffloats); @@ -590,7 +669,7 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< vector PCfrSolver::showdownUtility(int player, shared_ptr node, const vector &reach_probs, - int iter, uint64_t current_board,int deal) { + int iter, uint64_t current_board,int deal, const string& path) { // player win时候player的收益,player lose的时候收益明显为-player_payoff int oppo = 1 - player; float win_payoff = node->get_payoffs(ShowdownNode::ShowDownResult::NOTTIE,player,player); @@ -607,10 +686,10 @@ PCfrSolver::showdownUtility(int player, shared_ptr node, const vec 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){ + while (j < oppo_combs.size() && one_player_comb.rank < oppo_combs[j].rank) { const RiverCombs& one_oppo_comb = oppo_combs[j]; winsum += reach_probs[one_oppo_comb.reach_prob_index]; card_winsum[one_oppo_comb.private_cards.card1] += reach_probs[one_oppo_comb.reach_prob_index]; @@ -628,15 +707,15 @@ PCfrSolver::showdownUtility(int player, shared_ptr node, const vec vector& card_losssum = card_winsum; fill(card_losssum.begin(),card_losssum.end(),0); - j = oppo_combs.size() - 1; + ptrdiff_t j_signed = static_cast(oppo_combs.size()) - 1; for(int i = 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_signed >= 0 && one_player_comb.rank > oppo_combs[j_signed].rank){ + const RiverCombs& one_oppo_comb = oppo_combs[j_signed]; 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_signed--; } payoffs[one_player_comb.reach_prob_index] += (losssum - card_losssum[one_player_comb.private_cards.card1] @@ -648,7 +727,7 @@ PCfrSolver::showdownUtility(int player, shared_ptr node, const vec vector PCfrSolver::terminalUtility(int player, shared_ptr node, const vector &reach_prob, int iter, - uint64_t current_board,int deal) { + uint64_t current_board,int deal, const string& path) { float player_payoff = node->get_payoffs()[player]; int oppo = 1 - player; @@ -766,7 +845,22 @@ void PCfrSolver::stop() { this->nowstop = true; } -void PCfrSolver::train() { +void PCfrSolver::train(const vector& locked_nodes, const std::optional& full_board) { + this->m_locked_nodes_map.clear(); + if (!locked_nodes.empty()) { + qDebug().noquote() << "Node locking enabled for" << locked_nodes.size() << "rules."; + for (const auto& locked_node : locked_nodes) { + this->m_locked_nodes_map[locked_node.node_path] = &locked_node; + qDebug().noquote() << " - Locking node path:" << QString::fromStdString(locked_node.node_path) << "for player" << locked_node.player_to_lock; + } + } + this->m_full_board_situation = full_board; + if (m_full_board_situation.has_value()) { + qDebug().noquote() << "Full board analysis enabled."; + if (this->warmup > 0) { + qDebug().noquote() << " - Isomorphic deal expansion will be disabled during warmup iterations."; + } + } vector> player_privates(this->player_number); player_privates[0] = pcm.getPreflopCards(0); @@ -794,7 +888,7 @@ void PCfrSolver::train() { //#pragma omp single { //this->distributing_task = true; - cfr(player_id, this->tree->getRoot(), reach_probs[1 - player_id], i, this->initial_board_long,0); + cfr(player_id, this->tree->getRoot(), reach_probs[1 - player_id], i, this->initial_board_long,0, ""); //throw runtime_error("returning..."); } } @@ -832,7 +926,7 @@ void PCfrSolver::train() { //#pragma omp single { //this->distributing_task = true; - cfr(player_id, this->tree->getRoot(), reach_probs[1 - player_id], this->iteration_number, this->initial_board_long,0); + cfr(player_id, this->tree->getRoot(), reach_probs[1 - player_id], this->iteration_number, this->initial_board_long,0, ""); } } } @@ -1009,18 +1103,43 @@ void PCfrSolver::reConvertJson(const shared_ptr& node,json& strate } } -vector>> PCfrSolver::get_strategy(shared_ptr node,vector chance_cards){ +vector>> PCfrSolver::get_strategy(shared_ptr node,vector chance_cards, const std::string& path){ + // Check for locked node first, and return the fixed strategy if found. + auto it = m_locked_nodes_map.find(path); + if (it != m_locked_nodes_map.end() && it->second->player_to_lock == node->getPlayer()) { + vector>> ret_strategy(52, vector>(52)); + const Strategy& locked_strategy = it->second->locked_strategy; + const auto& actions = node->getActions(); + + for (const auto& private_card : ranges[node->getPlayer()]) { + vector hand_strategy(actions.size(), 0.0f); + for (size_t action_id = 0; action_id < actions.size(); ++action_id) { + const GameActions& game_action = actions[action_id]; + Action locked_action_key; + switch(game_action.getAction()) { + case GameTreeNode::PokerActions::FOLD: locked_action_key = -1; break; + case GameTreeNode::PokerActions::CHECK: + case GameTreeNode::PokerActions::CALL: locked_action_key = 0; break; + case GameTreeNode::PokerActions::BET: + case GameTreeNode::PokerActions::RAISE: locked_action_key = static_cast(game_action.getAmount()); break; + default: continue; // Should not happen + } + auto strat_it = locked_strategy.find(locked_action_key); + if (strat_it != locked_strategy.end()) { + hand_strategy[action_id] = strat_it->second; + } + } + ret_strategy[private_card.card1][private_card.card2] = hand_strategy; + } + return ret_strategy; + } + + // --- Original logic if node is not locked --- int deal = 0; int card_num = this->deck.getCards().size(); vector> exchange_color_list; vector>> ret_strategy = vector>>(52); - for(int i = 0;i < 52;i ++){ - ret_strategy[i] = vector>(52); - for(int j = 0;j < 52;j ++){ - ret_strategy[i][j] = vector(); - } - } vector& cards = this->deck.getCards(); @@ -1092,7 +1211,7 @@ vector>> PCfrSolver::get_strategy(shared_ptr no return ret_strategy; } -vector>> PCfrSolver::get_evs(shared_ptr node,vector chance_cards){ +vector>> PCfrSolver::get_evs(shared_ptr node,vector chance_cards, const std::string& path){ // If solving process has not finished, then no evs is set, therefore we shouldn't return anything int deal = 0; int card_num = this->deck.getCards().size(); @@ -1184,4 +1303,3 @@ json PCfrSolver::dumps(bool with_status,int depth) { this->reConvertJson(this->tree->getRoot(),retjson,"",0,depth,vector({"begin"}),0,vector>()); return std::move(retjson); } - diff --git a/src/ui/detailitemdelegate.cpp b/src/ui/detailitemdelegate.cpp index 4d6040b2..292a2d59 100644 --- a/src/ui/detailitemdelegate.cpp +++ b/src/ui/detailitemdelegate.cpp @@ -16,6 +16,8 @@ void DetailItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionVie initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); options.text = ""; @@ -24,7 +26,7 @@ void DetailItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionVie shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); 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(); + strategy_number = static_cast(detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size()); } int ind = index.row() * detailViewerModel->columns + index.column(); @@ -107,33 +109,33 @@ void DetailItemDelegate::paint_strategy(QPainter *painter, const QStyleOptionVie 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; } } + } 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)); + if(one_action.getAction() == GameTreeNode::PokerActions::FOLD) { + 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)); + else if(one_action.getAction() == GameTreeNode::PokerActions::CALL) { + 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)); + else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK) { + 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)); + else if(one_action.getAction() == GameTreeNode::PokerActions::BET) { + 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)); + else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE) { + options.text += QString("
%1 %2 : %3%%
").arg(tr("RAISE"),QString::number(one_action.getAmount()),QString::number(one_strategy,'f',1)); } } } @@ -155,6 +157,8 @@ void DetailItemDelegate::paint_range(QPainter *painter, const QStyleOptionViewIt initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); options.text = ""; @@ -191,9 +195,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)); } } @@ -210,6 +214,8 @@ void DetailItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewItem initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + options.text = ""; if(detailViewerModel->tableStrategyModel->treeItem != NULL && @@ -217,7 +223,7 @@ void DetailItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewItem shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); 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(); + strategy_number = static_cast(detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size()); } int ind = index.row() * detailViewerModel->columns + index.column(); @@ -310,35 +316,35 @@ void DetailItemDelegate::paint_evs(QPainter *painter, const QStyleOptionViewItem 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; } } + } 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); + if(one_action.getAction() == GameTreeNode::PokerActions::FOLD) { + 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); + else if(one_action.getAction() == GameTreeNode::PokerActions::CALL) { + 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); + else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK) { + 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); + else if(one_action.getAction() == GameTreeNode::PokerActions::BET) { + 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); + else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE) { + options.text += QString("
%1 %2 %3: %4
").arg(tr("RAISE"),QString::number(one_action.getAmount()),ev_str,one_ev); } } } @@ -360,6 +366,8 @@ void DetailItemDelegate::paint_evs_only(QPainter *painter, const QStyleOptionVie initStyleOption(&options, index); const DetailViewerModel * detailViewerModel = qobject_cast(index.model()); + if (!detailViewerModel || !detailViewerModel->tableStrategyModel) return; + //vector> strategy = detailViewerModel->tableStrategyModel->get_strategy(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); options.text = ""; @@ -369,7 +377,7 @@ void DetailItemDelegate::paint_evs_only(QPainter *painter, const QStyleOptionVie shared_ptr node = detailViewerModel->tableStrategyModel->treeItem->m_treedata.lock(); 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(); + strategy_number = static_cast(detailViewerModel->tableStrategyModel->ui_strategy_table[this->detailWindowSetting->grid_i][this->detailWindowSetting->grid_j].size()); } vector evs = detailViewerModel->tableStrategyModel->get_ev_grid(this->detailWindowSetting->grid_i,this->detailWindowSetting->grid_j); @@ -398,9 +406,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)); } } diff --git a/src/ui/detailviewermodel.cpp b/src/ui/detailviewermodel.cpp index 44d1ab9c..600bdb12 100644 --- a/src/ui/detailviewermodel.cpp +++ b/src/ui/detailviewermodel.cpp @@ -2,6 +2,10 @@ DetailViewerModel::DetailViewerModel(TableStrategyModel* tableStrategyModel, QObject *parent):QAbstractItemModel(parent){ this->tableStrategyModel = tableStrategyModel; + // This connection is the key to fixing the crash. When the tableStrategyModel is + // about to be destroyed, it will emit a signal, and this lambda will set our + // pointer to nullptr, preventing any use-after-free errors. + connect(tableStrategyModel, &QObject::destroyed, this, [this](){ this->tableStrategyModel = nullptr; }); this->columns = 4; this->rows = 3; } diff --git a/src/ui/roughstrategyitemdelegate.cpp b/src/ui/roughstrategyitemdelegate.cpp index 5f24aa7a..2ea59f06 100644 --- a/src/ui/roughstrategyitemdelegate.cpp +++ b/src/ui/roughstrategyitemdelegate.cpp @@ -53,21 +53,21 @@ void RoughStrategyItemDelegate::paint_strategy(QPainter *painter, const QStyleOp GameActions one_action = one_strategy.first; float one_strategy_float = one_strategy.second.second * 100; float one_combo = one_strategy.second.first; - if(one_action.getAction() == GameTreeNode::PokerActions::FOLD){ - options.text += QString("

%1 <\/h3>

%2\%<\/h4>

%3 %4<\/h4>").arg(tr("FOLD"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); + if(one_action.getAction() == GameTreeNode::PokerActions::FOLD) { + options.text += QString("

%1

%2%%

%3 %4

").arg(tr("FOLD"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); } - else if(one_action.getAction() == GameTreeNode::PokerActions::CALL){ - options.text += QString("

%1 <\/h3>

%2\%<\/h4>

%3 %4<\/h4>").arg(tr("CALL"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); + else if(one_action.getAction() == GameTreeNode::PokerActions::CALL) { + options.text += QString("

%1

%2%%

%3 %4

").arg(tr("CALL"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); } - else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK){ - options.text += QString("

%1 <\/h3>

%2\%<\/h4>

%3 %4<\/h4>").arg(tr("CHECK"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); + else if(one_action.getAction() == GameTreeNode::PokerActions::CHECK) { + options.text += QString("

%1

%2%%

%3 %4

").arg(tr("CHECK"),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); } - else if(one_action.getAction() == GameTreeNode::PokerActions::BET){ - options.text += QString("

%1 %2 <\/h3>

%3\%<\/h4>

%4 %5<\/h4>").arg(tr("BET"),QString::number(one_action.getAmount(),'f',1),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); + else if(one_action.getAction() == GameTreeNode::PokerActions::BET) { + options.text += QString("

%1 %2

%3%%

%4 %5

").arg(tr("BET"),QString::number(one_action.getAmount(),'f',1),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); } - else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE){ - options.text += QString("

%1 %2 <\/h3>

%3\%<\/h4>

%4 %5<\/h4>").arg(tr("RAISE"),QString::number(one_action.getAmount(),'f',1),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); + else if(one_action.getAction() == GameTreeNode::PokerActions::RAISE) { + options.text += QString("

%1 %2

%3%%

%4 %5

").arg(tr("RAISE"),QString::number(one_action.getAmount(),'f',1),QString::number(one_strategy_float,'f',1),QString::number(one_combo,'f',1),tr("combos")); } } diff --git a/src/ui/roughstrategyviewermodel.cpp b/src/ui/roughstrategyviewermodel.cpp index 484a1bfa..0d4210a8 100644 --- a/src/ui/roughstrategyviewermodel.cpp +++ b/src/ui/roughstrategyviewermodel.cpp @@ -2,6 +2,10 @@ RoughStrategyViewerModel::RoughStrategyViewerModel(TableStrategyModel* tableStrategyModel, QObject *parent):QAbstractItemModel(parent){ this->tableStrategyModel = tableStrategyModel; + // This connection is the key to fixing the crash. When the tableStrategyModel is + // about to be destroyed, it will emit a signal, and this lambda will set our + // pointer to nullptr, preventing any use-after-free errors. + connect(tableStrategyModel, &QObject::destroyed, this, [this](){ this->tableStrategyModel = nullptr; }); } RoughStrategyViewerModel::~RoughStrategyViewerModel() @@ -25,12 +29,16 @@ QModelIndex RoughStrategyViewerModel::parent(const QModelIndex &child) const{ } void RoughStrategyViewerModel::onchanged(){ + // Add a guard to prevent emitting signals if the underlying model is gone. + if (!tableStrategyModel) return; emit headerDataChanged(Qt::Horizontal, 0 , columnCount()); } int RoughStrategyViewerModel::columnCount(const QModelIndex &parent) const { - return this->tableStrategyModel->total_strategy.size(); + // Add a guard to prevent accessing the deleted model. + if (!tableStrategyModel) return 0; + return static_cast(this->tableStrategyModel->total_strategy.size()); } int RoughStrategyViewerModel::rowCount(const QModelIndex &parent) const @@ -40,6 +48,9 @@ int RoughStrategyViewerModel::rowCount(const QModelIndex &parent) const QVariant RoughStrategyViewerModel::data(const QModelIndex &index, int role) const { + // Add a guard to prevent accessing the deleted model. + if (!tableStrategyModel) return QVariant(); + std::size_t col = index.column(); if(col < this->tableStrategyModel->total_strategy.size()){ pair> one_strategy = this->tableStrategyModel->total_strategy[col]; diff --git a/src/ui/tablestrategymodel.cpp b/src/ui/tablestrategymodel.cpp index 99d67817..d8620fc4 100644 --- a/src/ui/tablestrategymodel.cpp +++ b/src/ui/tablestrategymodel.cpp @@ -4,7 +4,68 @@ TableStrategyModel::TableStrategyModel(QSolverJob * data, QObject *parent) : QAbstractItemModel(parent) { this->qSolverJob = data; - setupModelData(); + // --- One-time static data setup --- + 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)); + } + + 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()); + } + + 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()); + } + + vector& p1range = this->qSolverJob->get_solver()->player1Range; + vector& p2range = this->qSolverJob->get_solver()->player2Range; + + for(const auto& one_private: p1range){ + const Card& card1 = this->cardint2card[one_private.card1]; + const Card& card2 = this->cardint2card[one_private.card2]; + + int rank1 = card1.getCardInt() / 4; + int suit1 = card1.getCardInt() % 4; + int index1 = 12 - rank1; + + int rank2 = card2.getCardInt() / 4; + int suit2 = card2.getCardInt() % 4; + int index2 = 12 - rank2; + + 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)); + } + } + for(const auto& one_private: p2range){ + const Card& card1 = this->cardint2card[one_private.card1]; + const Card& card2 = this->cardint2card[one_private.card2]; + + int rank1 = card1.getCardInt() / 4; + int suit1 = card1.getCardInt() % 4; + int index1 = 12 - rank1; + + int rank2 = card2.getCardInt() / 4; + int suit2 = card2.getCardInt() % 4; + int index2 = 12 - rank2; + + if(index1 == index2){ + this->ui_p2_range[index1][index2].push_back(std::pair(one_private.card1,one_private.card2)); + } + 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)); + } + } } TableStrategyModel::~TableStrategyModel() @@ -29,12 +90,12 @@ QModelIndex TableStrategyModel::parent(const QModelIndex &child) const{ int TableStrategyModel::columnCount(const QModelIndex &parent) const { - return this->qSolverJob->get_solver()->get_deck()->getRanks().size(); + return static_cast(this->qSolverJob->get_solver()->get_deck()->getRanks().size()); } int TableStrategyModel::rowCount(const QModelIndex &parent) const { - return this->qSolverJob->get_solver()->get_deck()->getRanks().size(); + return static_cast(this->qSolverJob->get_solver()->get_deck()->getRanks().size()); } QVariant TableStrategyModel::data(const QModelIndex &index, int role) const @@ -57,110 +118,38 @@ QVariant TableStrategyModel::data(const QModelIndex &index, int role) const return retval; } -void TableStrategyModel::setupModelData() +void TableStrategyModel::resetDynamicData() { - 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)); - } - - 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>(); - } - } - - 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; - } - } - - 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; - } - } - - 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>(); - } - } - - 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>(); - } - } - - vector& p1range = this->qSolverJob->get_solver()->player1Range; - vector& p2range = this->qSolverJob->get_solver()->player2Range; - - for(PrivateCards one_private: p1range){ - Card card1 = this->cardint2card[one_private.card1]; - Card card2 = this->cardint2card[one_private.card2]; - - 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 + size_t rank_size = ranks.size(); - int rank2 = card2.getCardInt() / 4; - int suit2 = card2.getCardInt() - (rank2)*4; - int index2 = 12 - rank2; + // Clear and resize ui_strategy_table + this->ui_strategy_table.assign(rank_size, vector>>(rank_size)); - 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)); - } - } - for(PrivateCards one_private: p2range){ - Card card1 = this->cardint2card[one_private.card1]; - Card card2 = this->cardint2card[one_private.card2]; - - 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_p2_range[index1][index2].push_back(std::pair(one_private.card1,one_private.card2)); - } - 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)); - } - } - this->total_strategy = vector>>(); + // Clear other dynamic members + this->total_strategy.clear(); + this->current_strategy.clear(); + this->current_evs.clear(); + this->p1_range.assign(52, vector(52, 0.0f)); + this->p2_range.assign(52, vector(52, 0.0f)); } void TableStrategyModel::clicked_event(const QModelIndex & index){ } -void TableStrategyModel::setGameTreeNode(TreeItem* treeNode){ +void TableStrategyModel::setGameTreeNode(TreeItem* treeNode) { this->treeItem = treeNode; } -void TableStrategyModel::setTrunCard(Card turn_card){ +Card TableStrategyModel::getTurnCard() const { + return this->turn_card; +} + +Card TableStrategyModel::getRiverCard() const { + return this->river_card; +} + +void TableStrategyModel::setTurnCard(Card turn_card){ this->turn_card = turn_card; } @@ -171,7 +160,7 @@ void TableStrategyModel::setRiverCard(Card river_card){ void TableStrategyModel::updateStrategyData(){ if(this->treeItem != NULL){ shared_ptr node = this->treeItem->m_treedata.lock(); - this->setupModelData(); + this->resetDynamicData(); if(node != nullptr && node->getType() == GameTreeNode::GameTreeNode::ACTION){ shared_ptr actionNode = dynamic_pointer_cast(node); //actionNode->getTrainable(); @@ -180,18 +169,26 @@ void TableStrategyModel::updateStrategyData(){ 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);} + if (root_round == GameTreeNode::GameRound::FLOP) { + if (current_round == GameTreeNode::GameRound::TURN) { + if (!this->turn_card.empty()) deal_cards.push_back(this->turn_card); + } + if (current_round == GameTreeNode::GameRound::RIVER) { + if (!this->turn_card.empty()) deal_cards.push_back(this->turn_card); + if (!this->river_card.empty()) 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);} + else if (root_round == GameTreeNode::GameRound::TURN) { + if (current_round == GameTreeNode::GameRound::RIVER) { + if (!this->river_card.empty()) 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); + std::string path = this->treeItem->getActionPath(); + vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(actionNode,deal_cards, path); this->current_strategy = current_strategy; - vector>> current_evs = this->qSolverJob->get_solver()->get_solver()->get_evs(actionNode,deal_cards); + vector>> current_evs = this->qSolverJob->get_solver()->get_solver()->get_evs(actionNode,deal_cards, path); this->current_evs = current_evs; for(int i = 0;i < 52;i ++){ @@ -230,21 +227,30 @@ void TableStrategyModel::updateStrategyData(){ shared_ptr iter_node = node->getParent(); shared_ptr last_node = node; - while(iter_node != nullptr){ - if(iter_node->getType() == GameTreeNode::GameTreeNode::ACTION){ + TreeItem* iter_tree_item = this->treeItem->parentItem(); + while(iter_node != nullptr && iter_tree_item != 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);} + if (root_round == GameTreeNode::GameRound::FLOP) { + if (current_round == GameTreeNode::GameRound::TURN) { + if (!this->turn_card.empty()) deal_cards.push_back(this->turn_card); + } + if (current_round == GameTreeNode::GameRound::RIVER) { + if (!this->turn_card.empty()) deal_cards.push_back(this->turn_card); + if (!this->river_card.empty()) 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);} + else if (root_round == GameTreeNode::GameRound::TURN) { + if (current_round == GameTreeNode::GameRound::RIVER) { + if (!this->river_card.empty()) deal_cards.push_back(this->river_card); + } } - vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(iterActionNode,deal_cards); + std::string path = iter_tree_item->getActionPath(); + vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(iterActionNode,deal_cards, path); int child_chosen = -1; for(std::size_t i = 0;i < iterActionNode->getChildrens().size();i ++){ @@ -269,6 +275,7 @@ void TableStrategyModel::updateStrategyData(){ iter_node = iter_node->getParent(); last_node = last_node->getParent(); + iter_tree_item = iter_tree_item->parentItem(); } } if(this->qSolverJob->get_solver() != NULL && this->qSolverJob->get_solver()->get_solver() != NULL && node != nullptr){ diff --git a/src/ui/treeitem.cpp b/src/ui/treeitem.cpp index d7f53033..ad10c4dd 100644 --- a/src/ui/treeitem.cpp +++ b/src/ui/treeitem.cpp @@ -83,6 +83,37 @@ QVariant TreeItem::data() const return "NodeError"; } +std::string TreeItem::getActionPath() const { + // The model's rootItem is a dummy. Its parent is nullptr. + // The first real node's parent is this dummy root. + if (!m_parentItem || !m_parentItem->m_parentItem) { + return ""; + } + + std::string path_segment; + // We need to get the parent's GameTreeNode to find out which action led to this item. + shared_ptr parentNode = m_parentItem->m_treedata.lock(); + shared_ptr currentNode = m_treedata.lock(); + + if (parentNode && parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { + shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); + const auto& actions = parentActionNode->getActions(); + const auto& childrens = parentActionNode->getChildrens(); + for (size_t i = 0; i < childrens.size(); ++i) { + if (childrens[i] == currentNode) { + path_segment = actions[i].toString() + "/"; + break; + } + } + } + // CHANCE nodes are ignored in the path for locking purposes, so we just recurse. + else if (parentNode && parentNode->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { + return m_parentItem->getActionPath(); + } + + return m_parentItem->getActionPath() + path_segment; +} + bool TreeItem::setParentItem(TreeItem *item) { m_parentItem = item; diff --git a/strategyexplorer.cpp b/strategyexplorer.cpp index 0f471625..eb328dba 100644 --- a/strategyexplorer.cpp +++ b/strategyexplorer.cpp @@ -1,5 +1,5 @@ #include "strategyexplorer.h" -#include "ui_strategyexplorer.h" +#include "ui_strategyexplorer.h" // This must be after strategyexplorer.h #include "qstandarditemmodel.h" #include #include @@ -40,10 +40,12 @@ StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : // Initize strategy(rough) table this->tableStrategyModel = new TableStrategyModel(this->qSolverJob,this); this->ui->strategyTableView->setModel(this->tableStrategyModel); - this->delegate_strategy = new StrategyItemDelegate(this->qSolverJob,&(this->detailWindowSetting),this); - this->ui->strategyTableView->setItemDelegate(this->delegate_strategy); + auto delegate_strategy = new StrategyItemDelegate(this->qSolverJob, &(this->detailWindowSetting), this); + this->ui->strategyTableView->setItemDelegate(delegate_strategy); Deck* deck = this->qSolverJob->get_solver()->get_deck(); + this->ui->turnCardBox->addItem(tr("None")); + this->ui->riverCardBox->addItem(tr("None")); int index = 0; QString board_qstring = QString::fromStdString(this->qSolverJob->board); for(Card one_card: deck->getCards()){ @@ -52,30 +54,65 @@ StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : 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); + if(card_str_formatted.contains(QString::fromUtf8("♦️")) || + card_str_formatted.contains(QString::fromUtf8("♥️️"))){ + this->ui->turnCardBox->setItemData(index + 1, QBrush(Qt::red),Qt::ForegroundRole); + this->ui->riverCardBox->setItemData(index + 1, 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->ui->turnCardBox->setItemData(index + 1, QBrush(Qt::black),Qt::ForegroundRole); + this->ui->riverCardBox->setItemData(index + 1, 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(); + if(this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound() == GameTreeNode::GameRound::TURN){ + this->ui->turnCardBox->setEnabled(false); } else if(this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound() == GameTreeNode::GameRound::RIVER){ - this->ui->turnCardBox->clear(); - this->ui->riverCardBox->clear(); + this->ui->turnCardBox->setEnabled(false); + this->ui->riverCardBox->setEnabled(false); + } + + // If in full board analysis mode, pre-select the turn/river cards and disable the dropdowns. + if (this->qSolverJob->full_board_situation.has_value()) { + const auto& situation = this->qSolverJob->full_board_situation.value(); + if (situation.board_cards.size() == 5) { + int turn_card_int = -1; + int river_card_int = -1; + + GameTreeNode::GameRound root_round = this->qSolverJob->get_solver()->getGameTree()->getRoot()->getRound(); + + if (root_round == GameTreeNode::GameRound::FLOP) { + turn_card_int = situation.board_cards[3]; + river_card_int = situation.board_cards[4]; + this->ui->turnCardBox->setEnabled(false); + this->ui->riverCardBox->setEnabled(false); + } else if (root_round == GameTreeNode::GameRound::TURN) { + river_card_int = situation.board_cards[4]; + this->ui->riverCardBox->setEnabled(false); + } + + // Find and set the turn card index in the dropdown + if (turn_card_int != -1) { + for (int i = 0; i < this->cards.size(); ++i) { + if (this->cards[i].getCardInt() == turn_card_int) { + this->ui->turnCardBox->setCurrentIndex(i + 1); // +1 for "None" + break; + } + } + } + + // Find and set the river card index in the dropdown + if (river_card_int != -1) { + for (int i = 0; i < this->cards.size(); ++i) { + if (this->cards[i].getCardInt() == river_card_int) { + this->ui->riverCardBox->setCurrentIndex(i + 1); // +1 for "None" + break; + } + } + } + } } // Initize timer for strategy auto update @@ -86,27 +123,33 @@ StrategyExplorer::StrategyExplorer(QWidget *parent,QSolverJob * qSolverJob) : // On mouse event of strategy table connect(this->ui->strategyTableView,SIGNAL(itemMouseChange(int,int)),this,SLOT(onMouseMoveEvent(int,int))); - // Initize Detail Viewer window + // Initialize Detail Viewer window this->detailViewerModel = new DetailViewerModel(this->tableStrategyModel,this); this->ui->detailView->setModel(this->detailViewerModel); - this->detailItemItemDelegate = new DetailItemDelegate(&(this->detailWindowSetting),this); - this->ui->detailView->setItemDelegate(this->detailItemItemDelegate); + auto detailItemItemDelegate = new DetailItemDelegate(&(this->detailWindowSetting),this); + this->ui->detailView->setItemDelegate(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->ui->roughStrategyView->setItemDelegate(this->roughStrategyItemDelegate); + auto roughStrategyItemDelegate = new RoughStrategyItemDelegate(&(this->detailWindowSetting), this); + this->ui->roughStrategyView->setItemDelegate(roughStrategyItemDelegate); + + // Programmatically select and click the root node to show the initial strategy. + QModelIndex rootIndex = this->ui->gameTreeView->tree_model->index(0, 0, QModelIndex()); + if (rootIndex.isValid()) { + this->ui->gameTreeView->setCurrentIndex(rootIndex); + item_clicked(rootIndex); + } } StrategyExplorer::~StrategyExplorer() { + // The timer is a child of this QDialog, so it will be deleted automatically + // by Qt's parent-child mechanism. Explicitly stopping it is still good + // practice to prevent signals from being processed during destruction. + if (timer) timer->stop(); delete ui; - delete this->delegate_strategy; - delete this->tableStrategyModel; - delete this->detailViewerModel; - delete this->roughStrategyViewerModel; - delete this->timer; } void StrategyExplorer::item_expanded(const QModelIndex& index){ @@ -125,15 +168,15 @@ void StrategyExplorer::process_board(TreeItem* treeitem){ 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())); + if(treeitem != NULL && tableStrategyModel){ + if(treeitem->m_treedata.lock()->getRound() == GameTreeNode::GameRound::TURN && !this->tableStrategyModel->getTurnCard().empty()) { + cards.push_back(this->tableStrategyModel->getTurnCard()); } else if(treeitem->m_treedata.lock()->getRound() == GameTreeNode::GameRound::RIVER){ - if(!this->tableStrategyModel->getTrunCard().empty()) - cards.push_back(Card(this->tableStrategyModel->getTrunCard())); + if(!this->tableStrategyModel->getTurnCard().empty()) + cards.push_back(this->tableStrategyModel->getTurnCard()); if(!this->tableStrategyModel->getRiverCard().empty()) - cards.push_back(Card(this->tableStrategyModel->getRiverCard())); + cards.push_back(this->tableStrategyModel->getRiverCard()); } } this->ui->boardLabel->setText(QString("%1: ").arg(tr("board")) + Card::boardCards2html(cards)); @@ -165,12 +208,16 @@ void StrategyExplorer::item_clicked(const QModelIndex& index){ TreeItem * treeNode = static_cast(index.internalPointer()); this->process_treeclick(treeNode); this->process_board(treeNode); - this->tableStrategyModel->setGameTreeNode(treeNode); - this->tableStrategyModel->updateStrategyData(); - this->ui->strategyTableView->viewport()->update(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->triger_resize(); - this->ui->roughStrategyView->viewport()->update(); + if (tableStrategyModel) { + tableStrategyModel->setGameTreeNode(treeNode); + tableStrategyModel->updateStrategyData(); + ui->strategyTableView->viewport()->update(); + } + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) { + ui->roughStrategyView->triger_resize(); + ui->roughStrategyView->viewport()->update(); + } } catch (const runtime_error& error) { @@ -185,90 +232,119 @@ void StrategyExplorer::selection_changed(const QItemSelection &selected, void StrategyExplorer::on_turnCardBox_currentIndexChanged(int index) { - if(this->cards.size() > 0 && 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); + if (!tableStrategyModel) return; + + if (index == 0) { // "None" selected + this->tableStrategyModel->setTurnCard(Card()); + } else if (this->cards.size() > 0 && (index - 1) < this->cards.size()){ + this->tableStrategyModel->setTurnCard(this->cards[index - 1]); + } + + this->tableStrategyModel->updateStrategyData(); + if (roughStrategyViewerModel) this->roughStrategyViewerModel->onchanged(); + if (ui) this->ui->roughStrategyView->viewport()->update(); + this->process_board(this->tableStrategyModel->treeItem); + if (ui) { + this->ui->strategyTableView->viewport()->update(); + this->ui->detailView->viewport()->update(); } - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); } void StrategyExplorer::on_riverCardBox_currentIndexChanged(int index) { - if(this->cards.size() > 0 && 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); + if (!tableStrategyModel) return; + + if (index == 0) { // "None" selected + this->tableStrategyModel->setRiverCard(Card()); + } else if(this->cards.size() > 0 && (index - 1) < this->cards.size()){ + this->tableStrategyModel->setRiverCard(this->cards[index - 1]); + } + + this->tableStrategyModel->updateStrategyData(); + if (roughStrategyViewerModel) this->roughStrategyViewerModel->onchanged(); + if (ui) this->ui->roughStrategyView->viewport()->update(); + this->process_board(this->tableStrategyModel->treeItem); + if (ui) { + this->ui->strategyTableView->viewport()->update(); + this->ui->detailView->viewport()->update(); } - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); } void StrategyExplorer::update_second(){ - if(this->cards.size() > 0){ + // Add guards to all members accessed in this slot, as it can be called + // during the object's destruction sequence. + if(tableStrategyModel && tableStrategyModel->treeItem != nullptr){ this->tableStrategyModel->updateStrategyData(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) ui->roughStrategyView->viewport()->update(); this->process_board(this->tableStrategyModel->treeItem); } - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + } } void StrategyExplorer::onMouseMoveEvent(int i,int j){ this->detailWindowSetting.grid_i = i; this->detailWindowSetting.grid_j = j; - this->ui->detailView->viewport()->update(); - this->ui->strategyTableView->viewport()->update(); + if (ui) { + if (ui->detailView) ui->detailView->viewport()->update(); + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + } } void StrategyExplorer::on_strategyModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::STRATEGY; - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + } + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) ui->roughStrategyView->viewport()->update(); } void StrategyExplorer::on_ipRangeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::RANGE_IP; - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + } + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) ui->roughStrategyView->viewport()->update(); } void StrategyExplorer::on_oopRangeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::RANGE_OOP; - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + } + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) ui->roughStrategyView->viewport()->update(); } void StrategyExplorer::on_evModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::EV; - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); - this->ui->roughStrategyView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + if (ui->roughStrategyView) ui->roughStrategyView->viewport()->update(); + } } void StrategyExplorer::on_evOnlyModeButtom_clicked() { this->detailWindowSetting.mode = DetailWindowSetting::DetailWindowMode::EV_ONLY; - this->ui->strategyTableView->viewport()->update(); - this->ui->detailView->viewport()->update(); - this->roughStrategyViewerModel->onchanged(); - this->ui->roughStrategyView->viewport()->update(); + if (ui) { + if (ui->strategyTableView) ui->strategyTableView->viewport()->update(); + if (ui->detailView) ui->detailView->viewport()->update(); + } + if (roughStrategyViewerModel) roughStrategyViewerModel->onchanged(); + if (ui) ui->roughStrategyView->viewport()->update(); } diff --git a/strategyexplorer.h b/strategyexplorer.h index 8d664765..4919bca9 100644 --- a/strategyexplorer.h +++ b/strategyexplorer.h @@ -2,27 +2,17 @@ #define STRATEGYEXPLORER_H #include +#include #include -#include -#include -#include - #include "include/runtime/qsolverjob.h" -#include "QItemSelection" -#include "include/ui/worditemdelegate.h" #include "include/ui/tablestrategymodel.h" #include "include/ui/strategyitemdelegate.h" -#include "include/ui/detailwindowsetting.h" -#include "include/Card.h" #include "include/ui/detailviewermodel.h" #include "include/ui/detailitemdelegate.h" #include "include/ui/roughstrategyviewermodel.h" #include "include/ui/roughstrategyitemdelegate.h" -#include "include/nodes/GameTreeNode.h" -#include "include/nodes/ActionNode.h" -#include "include/nodes/ChanceNode.h" -#include "include/nodes/TerminalNode.h" -#include "include/nodes/ShowdownNode.h" +#include "include/ui/detailwindowsetting.h" +#include namespace Ui { class StrategyExplorer; @@ -33,29 +23,14 @@ class StrategyExplorer : public QDialog Q_OBJECT public: - explicit StrategyExplorer(QWidget *parent = 0,QSolverJob * qSolverJob=nullptr); + explicit StrategyExplorer(QWidget *parent, QSolverJob * qSolverJob); ~StrategyExplorer(); -private: - DetailWindowSetting detailWindowSetting; - QTimer *timer; - Ui::StrategyExplorer *ui; - QSolverJob * qSolverJob; - StrategyItemDelegate * delegate_strategy; - TableStrategyModel * tableStrategyModel; - DetailViewerModel * detailViewerModel; - DetailItemDelegate * detailItemItemDelegate; - RoughStrategyViewerModel * roughStrategyViewerModel; - RoughStrategyItemDelegate * roughStrategyItemDelegate; - vector cards; - void process_treeclick(TreeItem* treeitem); - void process_board(TreeItem* treeitem); -public slots: +private slots: void item_expanded(const QModelIndex& index); void item_clicked(const QModelIndex& index); void selection_changed(const QItemSelection &selected, - const QItemSelection &deselected); -private slots: + const QItemSelection &deselected); void on_turnCardBox_currentIndexChanged(int index); void on_riverCardBox_currentIndexChanged(int index); void update_second(); @@ -65,6 +40,18 @@ private slots: void on_oopRangeButtom_clicked(); void on_evModeButtom_clicked(); void on_evOnlyModeButtom_clicked(); + +private: + Ui::StrategyExplorer *ui; + QSolverJob * qSolverJob; + QPointer tableStrategyModel; + QPointer detailViewerModel; + QPointer roughStrategyViewerModel; + QPointer timer; + DetailWindowSetting detailWindowSetting; + vector cards; + void process_board(TreeItem* treeitem); + void process_treeclick(TreeItem* treeitem); }; -#endif // STRATEGYEXPLORER_H +#endif // STRATEGYEXPLORER_H \ No newline at end of file From b84bcf777defd8712b7daeeca605cef0ccc353ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Sat, 6 Sep 2025 18:38:02 -0400 Subject: [PATCH 5/7] option to save the new game analysis mode --- include/solver/PCfrSolver.h | 6 +- include/solver/Solver.h | 21 ++- include/tools/CommandLineTool.h | 6 + include/ui/tablestrategymodel.h | 1 + include/ui/treemodel.h | 22 +-- mainwindow.cpp | 30 ++++ src/runtime/qsolverjob.cpp | 21 ++- src/solver/PCfrSolver.cpp | 89 +++++++---- src/tools/CommandLineTool.cpp | 79 +++++++++- src/ui/tablestrategymodel.cpp | 84 +++++------ src/ui/treeitem.cpp | 60 ++++---- src/ui/treemodel.cpp | 259 ++++++++++++++++++++------------ strategyexplorer.cpp | 11 +- 13 files changed, 451 insertions(+), 238 deletions(-) diff --git a/include/solver/PCfrSolver.h b/include/solver/PCfrSolver.h index 1b1f31c5..cffb2c7f 100644 --- a/include/solver/PCfrSolver.h +++ b/include/solver/PCfrSolver.h @@ -95,9 +95,9 @@ class PCfrSolver:public Solver { ~PCfrSolver(); void train(const vector& locked_nodes, const std::optional& full_board) override; void stop() override; - json dumps(bool with_status,int depth) override; - vector>> get_strategy(shared_ptr node,vector chance_cards, const std::string& path) override; - vector>> get_evs(shared_ptr node,vector chance_cards, const std::string& path) override; + json dumps(bool with_status, int depth) override; + ActionStrategy get_strategy(shared_ptr node, vector chance_cards, const std::string& path) override; + ActionEVs get_evs(shared_ptr node, vector chance_cards, const std::string& path) override; private: // New members for analysis features map m_locked_nodes_map; diff --git a/include/solver/Solver.h b/include/solver/Solver.h index 9dcec0b4..44907003 100644 --- a/include/solver/Solver.h +++ b/include/solver/Solver.h @@ -4,12 +4,25 @@ #ifndef TEXASSOLVER_SOLVER_H #define TEXASSOLVER_SOLVER_H - - #include #include #include "solver_options.h" // For LockedNode +// Forward-declare to avoid include cycle +class GameActions; + +struct ActionStrategy { + std::vector actions; + // Strategy per hand: 52 x 52 x num_actions + std::vector>> strategy_per_hand; +}; + +struct ActionEVs { + std::vector actions; + // EVs per hand: 52 x 52 x num_actions + std::vector>> evs_per_hand; +}; + class Solver { public: enum MonteCarolAlg { @@ -38,8 +51,8 @@ class Solver { virtual void stop() = 0; virtual json dumps(bool with_status,int depth) = 0; - virtual vector>> get_strategy(shared_ptr node,vector cards, const std::string& path) = 0; - virtual vector>> get_evs(shared_ptr node,vector cards, const std::string& path) = 0; + virtual ActionStrategy get_strategy(shared_ptr node,vector cards, const std::string& path) = 0; + virtual ActionEVs get_evs(shared_ptr node,vector cards, const std::string& path) = 0; shared_ptr tree; }; diff --git a/include/tools/CommandLineTool.h b/include/tools/CommandLineTool.h index 5c8e31a7..0bf55a40 100644 --- a/include/tools/CommandLineTool.h +++ b/include/tools/CommandLineTool.h @@ -9,6 +9,8 @@ #include #include #include "include/runtime/PokerSolver.h" +#include "include/solver/solver_options.h" +#include using namespace std; class CommandLineTool{ @@ -18,6 +20,7 @@ class CommandLineTool{ void execFromFile(string input_file); void processCommand(string input); private: + void parseAndAddNodeLockRule(const std::string& rule_line); enum Mode{ HOLDEM, SHORTDECK @@ -42,6 +45,9 @@ class CommandLineTool{ int use_isomorphism=0; int print_interval=10; int dump_rounds = 1; + // Add these new members for analysis features + bool m_use_full_board_analysis = false; + std::map, LockedNode> m_locked_nodes_map; shared_ptr gtbs; }; diff --git a/include/ui/tablestrategymodel.h b/include/ui/tablestrategymodel.h index 7225897a..c6dd3dd6 100644 --- a/include/ui/tablestrategymodel.h +++ b/include/ui/tablestrategymodel.h @@ -40,6 +40,7 @@ class TableStrategyModel : public QAbstractItemModel 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 current_actions; vector> p1_range; // cardint(52) * cardint(52) vector> p2_range; // cardint(52) * cardint(52) vector>>> ui_strategy_table; // rank * rank * (id,id) diff --git a/include/ui/treemodel.h b/include/ui/treemodel.h index c5bce57e..694c5637 100644 --- a/include/ui/treemodel.h +++ b/include/ui/treemodel.h @@ -1,23 +1,18 @@ #ifndef TREEMODEL_H #define TREEMODEL_H -#include "include/ui/treeitem.h" #include #include #include +#include "include/ui/treeitem.h" #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" -//! [0] class TreeModel : public QAbstractItemModel { Q_OBJECT public: - explicit TreeModel(QSolverJob* data, QObject *parent = nullptr); + explicit TreeModel(QSolverJob* qSolverJob, QObject *parent = nullptr); ~TreeModel(); QVariant data(const QModelIndex &index, int role) const override; @@ -28,16 +23,15 @@ class TreeModel : public QAbstractItemModel const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; - void reGenerateTreeItem(GameTreeNode::GameRound round,TreeItem* node_to_process); + + // Public method to trigger lazy-loading of children + void populate(const QModelIndex &index); private: - QSolverJob* qSolverJob; - void setupModelData(); TreeItem *rootItem; - -public slots: - void clicked_event(const QModelIndex & index); + QSolverJob* qSolverJob; }; -#endif // TREEMODEL_H +#endif // TREEMODEL_H \ No newline at end of file diff --git a/mainwindow.cpp b/mainwindow.cpp index 723026ef..0cc273ee 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -144,6 +144,8 @@ void MainWindow::clear_all_params(){ this->ui->logIntervalText->clear(); this->ui->raiseLimitText->clear(); this->ui->useIsoCheck->setChecked(false); + this->ui->nodeLockingText->clear(); + this->ui->fullBoardAnalysisCheck->setChecked(false); } void MainWindow::import_from_file(QString fileName){ @@ -161,6 +163,7 @@ void MainWindow::import_from_file(QString fileName){ QTextStream s1(&file); content.append(s1.readAll()); this->clear_all_params(); + QString node_locking_rules = ""; 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")); @@ -266,7 +269,19 @@ void MainWindow::import_from_file(QString fileName){ this->ui->useIsoCheck->setChecked(false); } } + else if(getParams(one_line_content,"set_full_board_analysis") != "INVALID"){ + if(getParams(one_line_content,"set_full_board_analysis") == "1"){ + this->ui->fullBoardAnalysisCheck->setChecked(true); + }else{ + this->ui->fullBoardAnalysisCheck->setChecked(false); + } + } + else if(getParams(one_line_content,"add_node_lock_rule") != "INVALID"){ + if (!node_locking_rules.isEmpty()) node_locking_rules.append("\n"); + node_locking_rules.append(getParams(one_line_content,"add_node_lock_rule")); + } } + this->ui->nodeLockingText->setPlainText(node_locking_rules); this->update(); } @@ -373,6 +388,21 @@ void MainWindow::on_actionexport_triggered(){ }else{ out << "set_use_isomorphism 0" << "\n"; } + + if(this->ui->fullBoardAnalysisCheck->isChecked()){ + out << "set_full_board_analysis 1" << "\n"; + }else{ + out << "set_full_board_analysis 0" << "\n"; + } + + QString node_locking_text = ui->nodeLockingText->toPlainText(); + QStringList lines = node_locking_text.split('\n', Qt::SkipEmptyParts); + for (const QString& line : lines) { + QString trimmed_line = line.trimmed(); + if (trimmed_line.startsWith('#') || trimmed_line.isEmpty()) continue; + out << "add_node_lock_rule " << trimmed_line << "\n"; + } + out << "start_solve"; out << "\n"; diff --git a/src/runtime/qsolverjob.cpp b/src/runtime/qsolverjob.cpp index 11d26c4d..1db86142 100644 --- a/src/runtime/qsolverjob.cpp +++ b/src/runtime/qsolverjob.cpp @@ -1,6 +1,5 @@ #include "include/runtime/qsolverjob.h" - using namespace std; void QSolverJob:: setContext(QSTextEdit * textEdit){ @@ -95,12 +94,20 @@ void QSolverJob::stop(){ } void QSolverJob::solving(){ - // TODO 为什么ui上多次求解会积累memory?哪里leak了? - // TODO 为什么有时候会莫名闪退? - // NOTE: This requires modifying the PokerSolver::train method to accept - // the new analysis parameters (locked_nodes and full_board_situation). qDebug().noquote() << tr("Start Solving..");//.toStdString() << std::endl; + // --- Pre-solve Logging --- + if (this->full_board_situation.has_value()) { + qDebug() << "QSolverJob: Full board analysis is ENABLED."; + } else { + qDebug() << "QSolverJob: Full board analysis is DISABLED."; + } + if (!this->locked_nodes.empty()) { + qDebug() << "QSolverJob: Node locking is ENABLED with" << this->locked_nodes.size() << "rules."; + } else { + qDebug() << "QSolverJob: Node locking is DISABLED."; + } + if(this->mode == Mode::HOLDEM){ this->ps_holdem.train( this->range_ip, @@ -115,7 +122,7 @@ void QSolverJob::solving(){ this->use_isomorphism, this->use_halffloats, this->thread_number, - this->locked_nodes, + this->locked_nodes, // Pass locked nodes to the solver this->full_board_situation ); }else if(this->mode == Mode::SHORTDECK){ @@ -132,7 +139,7 @@ void QSolverJob::solving(){ this->use_isomorphism, this->use_halffloats, this->thread_number, - this->locked_nodes, + this->locked_nodes, // Pass locked nodes to the solver this->full_board_situation ); } diff --git a/src/solver/PCfrSolver.cpp b/src/solver/PCfrSolver.cpp index 99339ac8..1c977a75 100644 --- a/src/solver/PCfrSolver.cpp +++ b/src/solver/PCfrSolver.cpp @@ -292,9 +292,6 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< // has sequential chance nodes for multi-card streets like the flop. if (node->getRound() == GameTreeNode::GameRound::FLOP && full_board_cards.size() >= 3) { size_t num_dealt_on_street = num_cards_on_board - this->initial_board.size(); - if (iter == 0) { - qDebug().noquote() << "FLOP analysis: num_cards_on_board=" << num_cards_on_board << "initial_board.size()=" << this->initial_board.size() << "num_dealt_on_street=" << num_dealt_on_street; - } if (num_dealt_on_street < 3) { card_to_deal_int = full_board_cards[num_dealt_on_street]; } @@ -306,7 +303,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< if (card_to_deal_int != -1) { if (iter == 0) { // Log only on first iteration - qDebug().noquote() << "Pruning chance node at round" << node->getRound() << ". Dealing only card:" << Card::intCard2Str(card_to_deal_int).c_str(); + qDebug().noquote() << "ANALYSIS: Pruning chance node at round" << node->getRound() << ". Dealing only card:" << Card::intCard2Str(card_to_deal_int).c_str(); } for (size_t i = 0; i < node->getCards().size(); ++i) { if (node->getCards()[i].getCardInt() == card_to_deal_int) { @@ -317,6 +314,14 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< break; // Found our specific card } } + // If we are in full board mode and didn't find a valid card to deal, + // it means this path is inconsistent with the analysis. Prune it. + if (valid_cards.empty()) { + if (iter == 0) { + qDebug().noquote() << "ANALYSIS: Pruning branch. Could not find card" << Card::intCard2Str(card_to_deal_int).c_str() << "to deal at round" << node->getRound(); + } + return chance_utility; // Return empty/zero utility + } } } else { // Not in full board mode, use original logic. for(std::size_t card = 0;card < node->getCards().size();card ++) { @@ -528,20 +533,38 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< vector> all_action_utility(actions.size()); - vector> results(actions.size()); + vector> results(actions.size(), vector(this->ranges[player].size(), 0.0f)); for (std::size_t action_id = 0; action_id < actions.size(); action_id++) { string next_path = path + actions[action_id].toString() + "/"; if (node_player != player) { vector new_reach_prob = vector(reach_probs.size()); + bool is_action_possible = false; for (std::size_t hand_id = 0; hand_id < new_reach_prob.size(); hand_id++) { float strategy_prob = current_strategy[hand_id + action_id * node_player_private_cards.size()]; new_reach_prob[hand_id] = reach_probs[hand_id] * strategy_prob; + if (new_reach_prob[hand_id] > 0) { + is_action_possible = true; + } + } + if (!is_action_possible) { + continue; // Prune this branch } - //#pragma omp task shared(results,action_id) results[action_id] = this->cfr(player, children[action_id], new_reach_prob, iter, current_board, deal, next_path); }else { - //#pragma omp task shared(results,action_id) + // Pruning for the current player. If an action has 0% probability for all hands, + // it won't contribute to the final payoff, and we don't need to calculate its utility. + bool is_action_possible = false; + for (size_t hand_id = 0; hand_id < node_player_private_cards.size(); ++hand_id) { + if (current_strategy[hand_id + action_id * node_player_private_cards.size()] > 1e-6) { + is_action_possible = true; + break; + } + } + + if (!is_action_possible) { + continue; // Prune this branch + } results[action_id] = this->cfr(player, children[action_id], reach_probs, iter, current_board, deal, next_path); } @@ -847,6 +870,11 @@ void PCfrSolver::stop() { void PCfrSolver::train(const vector& locked_nodes, const std::optional& full_board) { this->m_locked_nodes_map.clear(); + if (full_board.has_value()) { + qDebug() << "PCfrSolver::train received full_board_situation."; + } else { + qDebug() << "PCfrSolver::train did NOT receive full_board_situation."; + } if (!locked_nodes.empty()) { qDebug().noquote() << "Node locking enabled for" << locked_nodes.size() << "rules."; for (const auto& locked_node : locked_nodes) { @@ -1103,18 +1131,21 @@ void PCfrSolver::reConvertJson(const shared_ptr& node,json& strate } } -vector>> PCfrSolver::get_strategy(shared_ptr node,vector chance_cards, const std::string& path){ +ActionStrategy PCfrSolver::get_strategy(shared_ptr node,vector chance_cards, const std::string& path){ + ActionStrategy result; + result.actions = node->getActions(); // Always use the full set of actions from the node. + result.strategy_per_hand.assign(52, vector>(52, vector(result.actions.size(), 0.0f))); + // Check for locked node first, and return the fixed strategy if found. auto it = m_locked_nodes_map.find(path); if (it != m_locked_nodes_map.end() && it->second->player_to_lock == node->getPlayer()) { - vector>> ret_strategy(52, vector>(52)); + result.strategy_per_hand.assign(52, vector>(52, vector(result.actions.size(), 0.0f))); const Strategy& locked_strategy = it->second->locked_strategy; - const auto& actions = node->getActions(); for (const auto& private_card : ranges[node->getPlayer()]) { - vector hand_strategy(actions.size(), 0.0f); - for (size_t action_id = 0; action_id < actions.size(); ++action_id) { - const GameActions& game_action = actions[action_id]; + vector hand_strategy(result.actions.size(), 0.0f); // Sized to full action list, init to 0 + for (size_t i = 0; i < result.actions.size(); ++i) { + const GameActions& game_action = result.actions.at(i); Action locked_action_key; switch(game_action.getAction()) { case GameTreeNode::PokerActions::FOLD: locked_action_key = -1; break; @@ -1122,16 +1153,16 @@ vector>> PCfrSolver::get_strategy(shared_ptr no case GameTreeNode::PokerActions::CALL: locked_action_key = 0; break; case GameTreeNode::PokerActions::BET: case GameTreeNode::PokerActions::RAISE: locked_action_key = static_cast(game_action.getAmount()); break; - default: continue; // Should not happen + default: continue; } auto strat_it = locked_strategy.find(locked_action_key); if (strat_it != locked_strategy.end()) { - hand_strategy[action_id] = strat_it->second; + hand_strategy.at(i) = strat_it->second; } } - ret_strategy[private_card.card1][private_card.card2] = hand_strategy; + result.strategy_per_hand[private_card.card1][private_card.card2] = hand_strategy; } - return ret_strategy; + return result; } // --- Original logic if node is not locked --- @@ -1139,8 +1170,6 @@ vector>> PCfrSolver::get_strategy(shared_ptr no int card_num = this->deck.getCards().size(); vector> exchange_color_list; - vector>> ret_strategy = vector>>(52); - vector& cards = this->deck.getCards(); for(Card one_card: chance_cards){ @@ -1206,25 +1235,19 @@ vector>> PCfrSolver::get_strategy(shared_ptr no } } if(intercept) continue; - ret_strategy[pc.card1][pc.card2] = one_strategy; + result.strategy_per_hand[pc.card1][pc.card2] = one_strategy; } - return ret_strategy; + return result; } -vector>> PCfrSolver::get_evs(shared_ptr node,vector chance_cards, const std::string& path){ - // If solving process has not finished, then no evs is set, therefore we shouldn't return anything +ActionEVs PCfrSolver::get_evs(shared_ptr node,vector chance_cards, const std::string& path){ + ActionEVs result; + result.actions = node->getActions(); // For now, EV actions always match node actions + result.evs_per_hand.assign(52, vector>(52, vector(result.actions.size(), 0.0f))); int deal = 0; int card_num = this->deck.getCards().size(); vector> exchange_color_list; - vector>> ret_evs = vector>>(52); - for(int i = 0;i < 52;i ++){ - ret_evs[i] = vector>(52); - for(int j = 0;j < 52;j ++){ - ret_evs[i][j] = vector(); - } - } - vector& cards = this->deck.getCards(); for(Card one_card: chance_cards){ @@ -1290,9 +1313,9 @@ vector>> PCfrSolver::get_evs(shared_ptr node,ve } } if(intercept) continue; - ret_evs[pc.card1][pc.card2] = one_evs; + result.evs_per_hand[pc.card1][pc.card2] = one_evs; } - return ret_evs; + return result; } json PCfrSolver::dumps(bool with_status,int depth) { diff --git a/src/tools/CommandLineTool.cpp b/src/tools/CommandLineTool.cpp index 03e81080..df30247d 100644 --- a/src/tools/CommandLineTool.cpp +++ b/src/tools/CommandLineTool.cpp @@ -3,6 +3,9 @@ // #include "include/tools/CommandLineTool.h" #include +#include +#include +#include "include/Card.h" CommandLineTool::CommandLineTool(string mode,string resource_dir) { string suits = "c,d,h,s"; @@ -56,6 +59,43 @@ CommandLineTool::CommandLineTool(string mode,string resource_dir) { */ } +void CommandLineTool::parseAndAddNodeLockRule(const std::string& rule_line) { + std::vector parts; + split(rule_line, ';', parts); + if (parts.size() != 4) { + std::cout << "Warning: Skipping invalid node lock rule (wrong format): " << rule_line << std::endl; + return; + } + + std::string path = parts[0]; + int player = std::stoi(parts[1]); + std::string action_str = parts[2]; + std::transform(action_str.begin(), action_str.end(), action_str.begin(), + [](unsigned char c){ return std::tolower(c); }); + double prob = std::stod(parts[3]) / 100.0; + + Action action_key; + if (action_str == "f" || action_str == "fold") { + action_key = -1; + } else if (action_str == "c" || action_str == "check" || action_str == "call") { + action_key = 0; + } else if (action_str.rfind("b_", 0) == 0 || action_str.rfind("bet_", 0) == 0 || action_str.rfind("bet ", 0) == 0) { + size_t pos = action_str.find_last_of("_ "); + action_key = static_cast(std::stof(action_str.substr(pos + 1))); + } else if (action_str.rfind("r_", 0) == 0 || action_str.rfind("raise_", 0) == 0 || action_str.rfind("raise ", 0) == 0) { + size_t pos = action_str.find_last_of("_ "); + action_key = static_cast(std::stof(action_str.substr(pos + 1))); + } else { + std::cout << "Warning: Skipping invalid action in node lock rule: " << action_str << std::endl; + return; + } + + auto map_key = std::make_pair(path, player); + m_locked_nodes_map[map_key].node_path = path; + m_locked_nodes_map[map_key].player_to_lock = player; + m_locked_nodes_map[map_key].locked_strategy[action_key] = prob; +} + void CommandLineTool::startWorking() { string input_line; while(cin) { @@ -154,8 +194,43 @@ 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_full_board_analysis"){ + this->m_use_full_board_analysis = (paramstr == "1"); + }else if(command == "add_node_lock_rule"){ + this->parseAndAddNodeLockRule(paramstr); }else if(command == "start_solve"){ cout << "<<>>" << endl; + + // Convert map to vector + vector locked_nodes; + for (const auto& [key, val] : m_locked_nodes_map) { + locked_nodes.push_back(val); + } + + // Create optional full board situation + std::optional full_board_situation = std::nullopt; + if (m_use_full_board_analysis) { + vector board_str_arr; + split(this->board, ',', board_str_arr); + if (board_str_arr.size() == 5) { + FullBoardSituation situation; + bool ok = true; + for (const auto& card_str : board_str_arr) { + if (!card_str.empty()) { + situation.board_cards.push_back(Card::strCard2int(card_str)); + } else { + ok = false; + } + } + if (ok && situation.board_cards.size() == 5) { + full_board_situation = situation; + } else { + cout << "Warning: Full board analysis enabled, but board does not contain 5 valid cards. Solving normally." << endl; + } + } else { + cout << "Warning: Full board analysis enabled, but board does not contain 5 cards. Solving normally." << endl; + } + } this->ps.train( this->range_ip, this->range_oop, @@ -168,7 +243,9 @@ void CommandLineTool::processCommand(string input) { this->accuracy, this->use_isomorphism, 0, // TODO: enable half float option for command line tool - this->thread_number + this->thread_number, + locked_nodes, + full_board_situation ); }else if(command == "dump_result"){ string output_file = paramstr; diff --git a/src/ui/tablestrategymodel.cpp b/src/ui/tablestrategymodel.cpp index d8620fc4..565e178d 100644 --- a/src/ui/tablestrategymodel.cpp +++ b/src/ui/tablestrategymodel.cpp @@ -131,6 +131,7 @@ void TableStrategyModel::resetDynamicData() this->current_strategy.clear(); this->current_evs.clear(); this->p1_range.assign(52, vector(52, 0.0f)); + this->current_actions.clear(); this->p2_range.assign(52, vector(52, 0.0f)); } @@ -185,15 +186,17 @@ void TableStrategyModel::updateStrategyData(){ } if(this->qSolverJob->get_solver() != NULL && this->qSolverJob->get_solver()->get_solver() != NULL){ std::string path = this->treeItem->getActionPath(); - vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(actionNode,deal_cards, path); - this->current_strategy = current_strategy; + ActionStrategy strategy_result = this->qSolverJob->get_solver()->get_solver()->get_strategy(actionNode,deal_cards, path); + this->current_strategy = strategy_result.strategy_per_hand; + this->current_actions = strategy_result.actions; - vector>> current_evs = this->qSolverJob->get_solver()->get_solver()->get_evs(actionNode,deal_cards, path); - this->current_evs = current_evs; + ActionEVs evs_result = this->qSolverJob->get_solver()->get_solver()->get_evs(actionNode,deal_cards, path); + // Assuming EV actions are always the full set from the node for now. + this->current_evs = evs_result.evs_per_hand; for(int i = 0;i < 52;i ++){ for(int j = 0;j < 52;j ++){ - const vector& one_strategy = this->current_strategy[i][j]; + const vector& one_strategy = this->current_strategy.at(i).at(j); if(one_strategy.empty())continue; Card card1 = this->cardint2card[i]; Card card2 = this->cardint2card[j]; @@ -250,10 +253,11 @@ void TableStrategyModel::updateStrategyData(){ } std::string path = iter_tree_item->getActionPath(); - vector>> current_strategy = this->qSolverJob->get_solver()->get_solver()->get_strategy(iterActionNode,deal_cards, path); + ActionStrategy strategy_result = this->qSolverJob->get_solver()->get_solver()->get_strategy(iterActionNode,deal_cards, path); + const auto& current_strategy = strategy_result.strategy_per_hand; int child_chosen = -1; - for(std::size_t i = 0;i < iterActionNode->getChildrens().size();i ++){ + for(std::size_t i = 0; i < iterActionNode->getChildrens().size(); i++){ if(iterActionNode->getChildrens()[i] == last_node){ child_chosen = i; break; @@ -262,12 +266,12 @@ void TableStrategyModel::updateStrategyData(){ 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(current_strategy.at(i).at(j).empty()) continue; if(iterActionNode->getPlayer() == 0){ // p1, IP - this->p1_range[i][j] *= current_strategy[i][j][child_chosen]; + this->p1_range[i][j] *= current_strategy.at(i).at(j).at(child_chosen); } else if(iterActionNode->getPlayer() == 1){ // p2, OOP - this->p2_range[i][j] *= current_strategy[i][j][child_chosen]; + this->p2_range[i][j] *= current_strategy.at(i).at(j).at(child_chosen); }else throw runtime_error("player not exist in tablestrategymodel"); } } @@ -293,11 +297,10 @@ const vector>> TableStrategyModel::get_total_ 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); + vector combos(this->current_actions.size(),0.0); + vector avg_strategy(this->current_actions.size(),0.0); float sum_strategy = 0; for(std::size_t index1 = 0;index1 < this->current_strategy.size() ;index1 ++){ @@ -305,9 +308,9 @@ const vector>> TableStrategyModel::get_total_ const vector& one_strategy = this->current_strategy[index1][index2]; if(one_strategy.empty())continue; - 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& range = (current_player == 0) ? this->p1_range : this->p2_range; + if(range.size() <= index1 || range.at(index1).size() <= index2) throw runtime_error(" index error when get range in tablestrategymodel"); + const float one_range = range.at(index1).at(index2); for(std::size_t i = 0;i < one_strategy.size(); i ++ ){ float one_prob = one_strategy[i]; @@ -316,18 +319,20 @@ const vector>> TableStrategyModel::get_total_ sum_strategy += one_prob * one_range; } - if(gameActions.size() != one_strategy.size()){ + if(this->current_actions.size() != one_strategy.size()){ cout << "index: " << index1 << " " << index2 << endl; - cout << "size not match between gameAction and stragegy: " << gameActions.size() << " " << one_strategy.size() << endl; + cout << "size not match between gameAction and stragegy: " << this->current_actions.size() << " " << one_strategy.size() << endl; throw runtime_error("size not match between gameAction and stragegy"); } } } - for(std::size_t i = 0;i < gameActions.size(); i ++ ){ - avg_strategy[i] = avg_strategy[i] / sum_strategy; + for(std::size_t i = 0;i < this->current_actions.size(); i ++ ){ + if (sum_strategy > 0) { + avg_strategy[i] = avg_strategy[i] / sum_strategy; + } pair statics = pair(combos[i],avg_strategy[i]); - pair> one_ret = pair>(gameActions[i],statics); + pair> one_ret = pair>(this->current_actions[i],statics); ret_strategy.push_back(one_ret); } return ret_strategy; @@ -349,9 +354,7 @@ const vector> TableStrategyModel::get_strategy(int i,int if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ shared_ptr actionNode = dynamic_pointer_cast(node); - vector& gameActions = actionNode->getActions(); - - vector strategies; + vector strategies(this->current_actions.size(), 0.0f); // get range data - initally copied from paint_range - could probably be integrated in loops below for efficiancy vector> card_cords; @@ -376,18 +379,14 @@ const vector> TableStrategyModel::get_strategy(int i,int 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()){ + if(this->current_actions.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; + cout << "size not match between gameAction and stragegy: " << this->current_actions.size() << " " << one_strategy.size() << endl; throw runtime_error("size not match between gameAction and stragegy"); } @@ -398,7 +397,7 @@ const vector> TableStrategyModel::get_strategy(int i,int } for(std::size_t indi = 0;indi < strategies.size();indi ++){ - ret_strategy.push_back(std::pair(actionNode->getActions()[indi], + ret_strategy.push_back(std::pair(this->current_actions[indi], strategies[indi])); } @@ -420,14 +419,6 @@ const vector TableStrategyModel::get_ev_grid(int i,int j)const{ if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ shared_ptr actionNode = dynamic_pointer_cast(node); - vector& gameActions = actionNode->getActions(); - -// vector strategies; - -// 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; @@ -435,10 +426,10 @@ const vector TableStrategyModel::get_ev_grid(int i,int j)const{ const vector& one_ev = this->current_evs[index1][index2]; if(one_ev.size() != one_strategy.size()) return vector(); - if(gameActions.size() != one_strategy.size()){ + if(this->current_actions.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; + cout << "size not match between gameAction and stragegy: " << this->current_actions.size() << " " << one_strategy.size() << endl; throw runtime_error("size not match between gameAction and stragegy"); } float one_ev_float = 0; @@ -469,13 +460,12 @@ const vector TableStrategyModel::get_strategies_evs(int i,int j)const{ if(node->getType() == GameTreeNode::GameTreeNode::ACTION){ shared_ptr actionNode = dynamic_pointer_cast(node); - vector& gameActions = actionNode->getActions(); vector strategy_p; - if(this->ui_strategy_table[i][j].size() > 0){ - ret_evs = vector(gameActions.size()); + if(!this->current_actions.empty() && this->ui_strategy_table[i][j].size() > 0){ + ret_evs = vector(this->current_actions.size()); std::fill(ret_evs.begin(), ret_evs.end(), 0.); - strategy_p = vector(gameActions.size()); + strategy_p = vector(this->current_actions.size()); std::fill(strategy_p.begin(), strategy_p.end(), 0.); } float range = 0; @@ -485,11 +475,11 @@ const vector TableStrategyModel::get_strategies_evs(int i,int j)const{ 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()){ + if(this->current_actions.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; + << one_ev.size() << " " << this->current_actions.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 ++){ diff --git a/src/ui/treeitem.cpp b/src/ui/treeitem.cpp index ad10c4dd..afbb50e8 100644 --- a/src/ui/treeitem.cpp +++ b/src/ui/treeitem.cpp @@ -1,4 +1,5 @@ #include "include/ui/treeitem.h" +#include TreeItem::TreeItem(weak_ptr data,TreeItem *parentItem) : m_parentItem(parentItem) @@ -52,8 +53,20 @@ 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 "Expired Node"; + + shared_ptr parentNode = currentNode->getParent(); + // Defensive check for corrupted tree structure + if (m_parentItem && m_parentItem->m_treedata.lock() != parentNode) { + qWarning() << "TreeItem data inconsistency: UI parent does not match data parent."; + return "Data Error"; + } + if (parentNode && parentNode == currentNode) { + qWarning() << "TreeItem data inconsistency: Node is its own parent (cycle detected)."; + return "Cyclic Node Error"; + } + if(parentNode == nullptr){ return TreeItem::get_round_str(currentNode->getRound()) + QObject::tr(" begin"); } @@ -84,34 +97,29 @@ QVariant TreeItem::data() const } std::string TreeItem::getActionPath() const { - // The model's rootItem is a dummy. Its parent is nullptr. - // The first real node's parent is this dummy root. - if (!m_parentItem || !m_parentItem->m_parentItem) { - return ""; - } - - std::string path_segment; - // We need to get the parent's GameTreeNode to find out which action led to this item. - shared_ptr parentNode = m_parentItem->m_treedata.lock(); - shared_ptr currentNode = m_treedata.lock(); - - if (parentNode && parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { - shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); - const auto& actions = parentActionNode->getActions(); - const auto& childrens = parentActionNode->getChildrens(); - for (size_t i = 0; i < childrens.size(); ++i) { - if (childrens[i] == currentNode) { - path_segment = actions[i].toString() + "/"; - break; + // Iterative implementation to build the path from this item up to the root + std::string reversed_path; + const TreeItem* current_item = this; + + while (current_item && current_item->m_parentItem) { + shared_ptr parentNode = current_item->m_parentItem->m_treedata.lock(); + shared_ptr currentNode = current_item->m_treedata.lock(); + if (parentNode && currentNode) { + if (parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { + shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); + const auto& actions = parentActionNode->getActions(); + const auto& childrens = parentActionNode->getChildrens(); + for (size_t i = 0; i < childrens.size(); ++i) { + if (childrens[i] == currentNode) { + reversed_path = actions[i].toString() + "/" + reversed_path; + break; + } + } } } + current_item = current_item->m_parentItem; } - // CHANCE nodes are ignored in the path for locking purposes, so we just recurse. - else if (parentNode && parentNode->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { - return m_parentItem->getActionPath(); - } - - return m_parentItem->getActionPath() + path_segment; + return reversed_path; } bool TreeItem::setParentItem(TreeItem *item) diff --git a/src/ui/treemodel.cpp b/src/ui/treemodel.cpp index 8c94f6c1..ea04679a 100644 --- a/src/ui/treemodel.cpp +++ b/src/ui/treemodel.cpp @@ -1,10 +1,12 @@ #include "include/ui/treemodel.h" -TreeModel::TreeModel(QSolverJob * data, QObject *parent) - : QAbstractItemModel(parent) +// Constructor and other model methods need to be implemented. +// The following is a minimal implementation based on the context. + +TreeModel::TreeModel(QSolverJob *qSolverJob, QObject *parent) + : QAbstractItemModel(parent), qSolverJob(qSolverJob) { - this->qSolverJob = data; - setupModelData(); + rootItem = new TreeItem(qSolverJob->get_solver()->get_game_tree()->getRoot()); } TreeModel::~TreeModel() @@ -12,59 +14,24 @@ TreeModel::~TreeModel() delete rootItem; } -int TreeModel::columnCount(const QModelIndex &parent) const -{ - if (parent.isValid()) - return static_cast(parent.internalPointer())->columnCount(); - return rootItem->columnCount(); -} - -QVariant TreeModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - if (role != Qt::DisplayRole) - return QVariant(); - - TreeItem *item = static_cast(index.internalPointer()); - - return item->data(); -} - -Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const -{ - if (!index.isValid()) - return Qt::NoItemFlags; - - return QAbstractItemModel::flags(index); -} - -QVariant TreeModel::headerData(int section, Qt::Orientation orientation, - int role) const -{ - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) - return rootItem->data(); - - return QVariant(); -} - QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { - if (!hasIndex(row, column, parent)) + if (!hasIndex(row, column, parent)) { return QModelIndex(); + } - TreeItem *parentItem; - - if (!parent.isValid()) - parentItem = rootItem; - else - parentItem = static_cast(parent.internalPointer()); + if (!parent.isValid()) { + // This is a request for a top-level item. + if (rootItem && row == 0) { + return createIndex(row, column, rootItem); + } + return QModelIndex(); + } + // This is a request for a child of a valid parent item. + TreeItem *parentItem = static_cast(parent.internalPointer()); TreeItem *childItem = parentItem->child(row); - if (childItem) - return createIndex(row, column, childItem); - return QModelIndex(); + return childItem ? createIndex(row, column, childItem) : QModelIndex(); } QModelIndex TreeModel::parent(const QModelIndex &index) const @@ -75,7 +42,7 @@ QModelIndex TreeModel::parent(const QModelIndex &index) const TreeItem *childItem = static_cast(index.internalPointer()); TreeItem *parentItem = childItem->parentItem(); - if (parentItem == rootItem) + if (parentItem == rootItem || !parentItem) return QModelIndex(); return createIndex(parentItem->row(), 0, parentItem); @@ -83,63 +50,161 @@ QModelIndex TreeModel::parent(const QModelIndex &index) const int TreeModel::rowCount(const QModelIndex &parent) const { - TreeItem *parentItem; - if (parent.column() > 0) + if (parent.column() > 0) { return 0; - - if (!parent.isValid()) - parentItem = rootItem; - else - parentItem = static_cast(parent.internalPointer()); - - return parentItem->childCount(); -} - -void TreeModel::reGenerateTreeItem(GameTreeNode::GameRound round,TreeItem* node_to_process){ - const shared_ptr gameTreeNode = node_to_process->m_treedata.lock(); - if(gameTreeNode->getType() == GameTreeNode::GameTreeNodeType::ACTION){ - shared_ptr actionNode = dynamic_pointer_cast(gameTreeNode); - vector>& childrens = actionNode->getChildrens(); - for(shared_ptr one_child:childrens){ - TreeItem * child_node = new TreeItem(one_child,node_to_process); - node_to_process->insertChild(child_node); - if(one_child->getRound() != round){ - continue; - } - this->reGenerateTreeItem(round,child_node); - } } - else if(gameTreeNode->getType() == GameTreeNode::GameTreeNodeType::CHANCE){ - shared_ptr chanceNode = dynamic_pointer_cast(gameTreeNode); - TreeItem * child_node = new TreeItem(chanceNode->getChildren(),node_to_process); - node_to_process->insertChild(child_node); - this->reGenerateTreeItem(round,child_node); + if (!parent.isValid()) { + return rootItem ? 1 : 0; // There is one root item. } + TreeItem *parentItem = static_cast(parent.internalPointer()); + return parentItem->childCount(); } -void TreeModel::setupModelData() +bool TreeModel::hasChildren(const QModelIndex &parent) const { - PokerSolver * solver; - if(this->qSolverJob->mode == QSolverJob::Mode::HOLDEM){ - solver = &(this->qSolverJob->ps_holdem); - }else if(this->qSolverJob->mode == QSolverJob::Mode::SHORTDECK){ - solver = &(this->qSolverJob->ps_shortdeck); - }else{ - throw runtime_error("holdem mode incorrect"); + // This function is key for lazy-loading. It tells the view whether to draw + // an expansion indicator, even if the children haven't been fetched yet. + + if (!parent.isValid()) { + // The invisible root of the model has one child (the game tree's root). + return rootItem != nullptr; } - if(solver->get_game_tree() == nullptr || solver->get_game_tree()->getRoot() == nullptr){ - return; + TreeItem *item = static_cast(parent.internalPointer()); + if (!item) return false; + + // If children are already populated, we can rely on childCount. + if (item->childCount() > 0) return true; + + // Otherwise, check the underlying data source to see if it can have children. + shared_ptr node = item->m_treedata.lock(); + if (!node) return false; + + if (node->getType() == GameTreeNode::GameTreeNodeType::ACTION) { + return !std::dynamic_pointer_cast(node)->getChildrens().empty(); + } else if (node->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { + return std::dynamic_pointer_cast(node)->getChildren() != nullptr; } - GameTreeNode::GameRound round = solver->get_game_tree()->getRoot()->getRound(); + return false; // Terminal/Showdown nodes have no children. +} - this->rootItem = new TreeItem(solver->get_game_tree()->getRoot()); - TreeItem* ti = new TreeItem(solver->get_game_tree()->getRoot(),this->rootItem); - rootItem->insertChild(ti); - this->reGenerateTreeItem(round,ti); +int TreeModel::columnCount(const QModelIndex &parent) const +{ + return 1; } +QVariant TreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || role != Qt::DisplayRole) + return QVariant(); + + TreeItem *item = static_cast(index.internalPointer()); + return item->data(); +} -void TreeModel::clicked_event(const QModelIndex & index){ +Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) return Qt::NoItemFlags; + return QAbstractItemModel::flags(index); } + +QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + return QVariant(tr("Game Tree")); + return QVariant(); +} + +void TreeModel::populate(const QModelIndex &index) +{ + TreeItem *item = static_cast(index.internalPointer()); + if (!item || item->childCount() > 0) { + return; // Already populated + } + + shared_ptr node = item->m_treedata.lock(); + if (!node) return; + + const auto& locked_nodes = this->qSolverJob->locked_nodes; + bool is_view_locked = !locked_nodes.empty(); + + if (node->getType() == GameTreeNode::GameTreeNodeType::ACTION) { + shared_ptr actionNode = dynamic_pointer_cast(node); + string path = item->getActionPath(); + const auto& actions = actionNode->getActions(); + const auto& childrens = actionNode->getChildrens(); + std::vector> children_to_add; + + if (!is_view_locked) { + // Normal mode: add all children + children_to_add = childrens; + } else { + // Locked mode: filter children based on rules + const LockedNode* lock_rule = nullptr; + for(const auto& rule : locked_nodes) { + if (rule.node_path == path && rule.player_to_lock == actionNode->getPlayer()) { + lock_rule = &rule; + break; + } + } + + for (size_t i = 0; i < actions.size(); ++i) { + bool add_child = false; + if (lock_rule) { + // A rule exists for this specific node, only add actions in the rule + Action action_key; + switch(actions[i].getAction()) { + case GameTreeNode::PokerActions::FOLD: action_key = -1; break; + case GameTreeNode::PokerActions::CHECK: case GameTreeNode::PokerActions::CALL: action_key = 0; break; + case GameTreeNode::PokerActions::BET: case GameTreeNode::PokerActions::RAISE: action_key = static_cast(actions[i].getAmount()); break; + default: continue; + } + if (lock_rule->locked_strategy.count(action_key)) { + add_child = true; + } + } else { + // No rule for this node, check if it's on a path to a future locked node + string child_path = path + actions[i].toString() + "/"; + for (const auto& rule : locked_nodes) { + if (rule.node_path.rfind(child_path, 0) == 0) { + add_child = true; + break; + } + } + } + if (add_child) { + children_to_add.push_back(childrens[i]); + } + } + } + + // Add the filtered children to the model + if (!children_to_add.empty()) { + beginInsertRows(index, 0, children_to_add.size() - 1); + for (const auto& child_node : children_to_add) { + // Defensive check to ensure child's parent pointer is correct. + if (!child_node->getParent() || child_node->getParent() != node) { + qWarning() << "Detected a child with an incorrect parent pointer. Skipping."; + continue; + } + // Defensive check to prevent infinite loops from cyclic trees + if (child_node == node) { + qWarning() << "Detected a cycle in the game tree. Skipping child."; + continue; + } + item->insertChild(new TreeItem(child_node, item)); + } + endInsertRows(); + } + } else if (node->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { + shared_ptr chanceNode = dynamic_pointer_cast(node); + shared_ptr childNode = chanceNode->getChildren(); + // Also check for cycles here + if (childNode && childNode != node) { + beginInsertRows(index, 0, 0); + item->insertChild(new TreeItem(childNode, item)); + endInsertRows(); + } + } +} \ No newline at end of file diff --git a/strategyexplorer.cpp b/strategyexplorer.cpp index eb328dba..0d030a7b 100644 --- a/strategyexplorer.cpp +++ b/strategyexplorer.cpp @@ -153,12 +153,11 @@ StrategyExplorer::~StrategyExplorer() } void StrategyExplorer::item_expanded(const QModelIndex& index){ - TreeItem *item = static_cast(index.internalPointer()); - int num_child = item->childCount(); - for (int i = 0;i < num_child;i ++){ - TreeItem* one_child = item->child(i); - if(one_child->childCount() != 0)continue; - this->ui->gameTreeView->tree_model->reGenerateTreeItem(one_child->m_treedata.lock()->getRound(),one_child); + // The logic for populating child nodes has been moved into the TreeModel + // to resolve the protected member access error. We just need to trigger it. + // Note: You will need to replace 'dynamic_cast' with the actual type of your model. + if (auto* model = dynamic_cast(this->ui->gameTreeView->model())) { + model->populate(index); } } From f41b888d8ac26ba211744401245ed6626dceee68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Sat, 6 Sep 2025 23:45:51 -0400 Subject: [PATCH 6/7] some changes to the prune tree --- include/solver/PCfrSolver.h | 6 ++-- src/solver/PCfrSolver.cpp | 10 +++--- src/ui/treeitem.cpp | 63 ++++++++++++++++++------------------- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/include/solver/PCfrSolver.h b/include/solver/PCfrSolver.h index cffb2c7f..9195f01e 100644 --- a/include/solver/PCfrSolver.h +++ b/include/solver/PCfrSolver.h @@ -13,11 +13,13 @@ #include #include "include/solver/Solver.h" #include +#include "include/solver/solver_options.h" #include "include/tools/lookup8.h" #include "include/tools/utils.h" #include #include -/* +#include +/* template class ThreadsafeQueue { std::queue queue_; @@ -100,7 +102,7 @@ class PCfrSolver:public Solver { ActionEVs get_evs(shared_ptr node, vector chance_cards, const std::string& path) override; private: // New members for analysis features - map m_locked_nodes_map; + std::unordered_map m_locked_nodes_map; std::optional m_full_board_situation; vector> ranges; diff --git a/src/solver/PCfrSolver.cpp b/src/solver/PCfrSolver.cpp index 1c977a75..377fef2b 100644 --- a/src/solver/PCfrSolver.cpp +++ b/src/solver/PCfrSolver.cpp @@ -169,7 +169,7 @@ vector PCfrSolver::getAllAbstractionDeal(int deal){ int origin_deal = int((deal - 1) / 4) * 4; for(int i = 0;i < 4;i ++){ int one_card = origin_deal + i + 1; - + 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; @@ -184,7 +184,7 @@ 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; - + 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; @@ -264,7 +264,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< int i_card = card_base * 4 + i; const Card& one_card = node->getCards()[i_card]; uint64_t card_long = Card::boardInt2long(one_card.getCardInt()); - if (i == cardr) { + if (i == cardr) { if (!Card::boardsHasIntercept(card_long, current_board)) { multiplier_num += 1; } @@ -399,7 +399,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< results[one_card.getNumberInDeckInt()] = child_utility; } } - + if (m_full_board_situation.has_value()) { // When pruning, there's no isomorphism, and only one valid card. if (!valid_cards.empty()) { @@ -1325,4 +1325,4 @@ json PCfrSolver::dumps(bool with_status,int depth) { json retjson; this->reConvertJson(this->tree->getRoot(),retjson,"",0,depth,vector({"begin"}),0,vector>()); return std::move(retjson); -} +} \ No newline at end of file diff --git a/src/ui/treeitem.cpp b/src/ui/treeitem.cpp index afbb50e8..9710e77a 100644 --- a/src/ui/treeitem.cpp +++ b/src/ui/treeitem.cpp @@ -57,7 +57,7 @@ QVariant TreeItem::data() const if (!currentNode) return "Expired Node"; shared_ptr parentNode = currentNode->getParent(); - // Defensive check for corrupted tree structure + // --- Defensive checks for corrupted tree structure --- if (m_parentItem && m_parentItem->m_treedata.lock() != parentNode) { qWarning() << "TreeItem data inconsistency: UI parent does not match data parent."; return "Data Error"; @@ -67,59 +67,58 @@ QVariant TreeItem::data() const return "Cyclic Node Error"; } - if(parentNode == nullptr){ + if (parentNode == nullptr) { return TreeItem::get_round_str(currentNode->getRound()) + QObject::tr(" begin"); } - if(parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION){ + + if (parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); - vector& actions = parentActionNode->getActions(); - 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(); + const auto& actions = parentActionNode->getActions(); + const auto& childrens = parentActionNode->getChildrens(); + for (std::size_t i = 0; i < childrens.size(); i++) { + if (childrens[i] == currentNode) { return (parentActionNode->getPlayer() == 0 ? QObject::tr("IP "):QObject::tr("OOP ")) + \ - TreeItem::get_game_action_str(actions[i].getAction(),actions[i].getAmount()); + TreeItem::get_game_action_str(actions[i].getAction(), actions[i].getAmount()); } } - }if(parentNode->getType() == GameTreeNode::GameTreeNodeType::CHANCE){ + } else if (parentNode->getType() == GameTreeNode::GameTreeNodeType::CHANCE) { shared_ptr chanceNode = dynamic_pointer_cast(parentNode); - if(chanceNode->getRound() == GameTreeNode::GameRound::FLOP){ - return QObject::tr("DEAL FLOP CARD"); - } - else if(chanceNode->getRound() == GameTreeNode::GameRound::TURN){ - return QObject::tr("DEAL TURN CARD"); - } - else if(chanceNode->getRound() == GameTreeNode::GameRound::RIVER){ - return QObject::tr("DEAL RIVER CARD"); - }else throw runtime_error("round not recognized"); + return QObject::tr("DEAL ") + get_round_str(chanceNode->getRound()) + QObject::tr(" CARD"); } + return "NodeError"; } std::string TreeItem::getActionPath() const { - // Iterative implementation to build the path from this item up to the root - std::string reversed_path; + std::list path_parts; const TreeItem* current_item = this; while (current_item && current_item->m_parentItem) { shared_ptr parentNode = current_item->m_parentItem->m_treedata.lock(); shared_ptr currentNode = current_item->m_treedata.lock(); - if (parentNode && currentNode) { - if (parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { - shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); - const auto& actions = parentActionNode->getActions(); - const auto& childrens = parentActionNode->getChildrens(); - for (size_t i = 0; i < childrens.size(); ++i) { - if (childrens[i] == currentNode) { - reversed_path = actions[i].toString() + "/" + reversed_path; - break; - } + if (parentNode && currentNode && parentNode->getType() == GameTreeNode::GameTreeNodeType::ACTION) { + shared_ptr parentActionNode = dynamic_pointer_cast(parentNode); + const auto& actions = parentActionNode->getActions(); + const auto& childrens = parentActionNode->getChildrens(); + for (size_t i = 0; i < childrens.size(); ++i) { + if (childrens[i] == currentNode) { + path_parts.push_front(actions[i].toString()); + break; } } } current_item = current_item->m_parentItem; } - return reversed_path; + + std::string result_path; // Use a stringstream for efficient concatenation + std::stringstream ss; + for (const auto& part : path_parts) { + ss << part << "/"; + } + result_path = ss.str(); + + // The root node's path is empty, otherwise paths have a trailing slash. + return result_path; } bool TreeItem::setParentItem(TreeItem *item) From cf0ca3dd0cdc5099811f4c062aef7681d3ab16c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20F=C3=B6rnges?= Date: Sat, 6 Sep 2025 23:55:00 -0400 Subject: [PATCH 7/7] some changes to the prune tree --- include/solver/PCfrSolver.h | 12 ++++-- src/solver/PCfrSolver.cpp | 75 ++++++++++++++++++----------------- src/ui/treemodel.cpp | 78 +++++++++++++++++++------------------ 3 files changed, 90 insertions(+), 75 deletions(-) diff --git a/include/solver/PCfrSolver.h b/include/solver/PCfrSolver.h index 9195f01e..bc5f763b 100644 --- a/include/solver/PCfrSolver.h +++ b/include/solver/PCfrSolver.h @@ -101,9 +101,15 @@ class PCfrSolver:public Solver { ActionStrategy get_strategy(shared_ptr node, vector chance_cards, const std::string& path) override; ActionEVs get_evs(shared_ptr node, vector chance_cards, const std::string& path) override; private: - // New members for analysis features - std::unordered_map m_locked_nodes_map; - std::optional m_full_board_situation; + struct AnalysisState { + bool enabled = false; + std::unordered_map locked_nodes_map; + std::optional full_board; + + bool isNodeLocked(const std::string& path, int player) const; + const Strategy* getLockedStrategy(const std::string& path, int player) const; + }; + AnalysisState m_analysis; vector> ranges; vector range1; diff --git a/src/solver/PCfrSolver.cpp b/src/solver/PCfrSolver.cpp index 377fef2b..22c511dd 100644 --- a/src/solver/PCfrSolver.cpp +++ b/src/solver/PCfrSolver.cpp @@ -15,6 +15,19 @@ PCfrSolver::~PCfrSolver(){ //cout << "Pcfr destroyed" << endl; } +bool PCfrSolver::AnalysisState::isNodeLocked(const std::string& path, int player) const { + auto it = locked_nodes_map.find(path); + return it != locked_nodes_map.end() && it->second->player_to_lock == player; +} + +const Strategy* PCfrSolver::AnalysisState::getLockedStrategy(const std::string& path, int player) const { + auto it = locked_nodes_map.find(path); + if (it != locked_nodes_map.end() && it->second->player_to_lock == player) { + return &it->second->locked_strategy; + } + return nullptr; +} + 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){ @@ -281,9 +294,9 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< vector valid_cards; valid_cards.reserve(node->getCards().size()); - if (m_full_board_situation.has_value()) { + if (m_analysis.full_board.has_value()) { // Full board analysis: only "deal" the specific card for this street. - const auto& full_board_cards = m_full_board_situation->board_cards; + const auto& full_board_cards = m_analysis.full_board->board_cards; vector current_board_vec = Card::long2board(current_board); size_t num_cards_on_board = current_board_vec.size(); int card_to_deal_int = -1; @@ -400,7 +413,7 @@ PCfrSolver::chanceUtility(int player, shared_ptr node, const vector< } } - if (m_full_board_situation.has_value()) { + if (m_analysis.full_board.has_value()) { // When pruning, there's no isomorphism, and only one valid card. if (!valid_cards.empty()) { int card_idx = valid_cards[0]; @@ -470,13 +483,13 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< bool is_locked = false; int node_player = node->getPlayer(); - auto it = m_locked_nodes_map.find(path); - if (it != m_locked_nodes_map.end() && it->second->player_to_lock == node_player) { + const Strategy* locked_strategy_ptr = m_analysis.getLockedStrategy(path, node_player); + if (locked_strategy_ptr) { is_locked = true; if (iter == 0) { // Log only on the first iteration to avoid spam qDebug().noquote() << "Applying locked strategy at path:" << QString::fromStdString(path); } - const Strategy& locked_strategy = it->second->locked_strategy; + const Strategy& locked_strategy = *locked_strategy_ptr; current_strategy.assign(actions.size() * node_player_private_cards.size(), 0.0f); for (size_t action_id = 0; action_id < actions.size(); ++action_id) { @@ -619,15 +632,10 @@ PCfrSolver::actionUtility(int player, shared_ptr node, const vector< if(!this->distributing_task && !this->collecting_statics && !is_locked) { 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 { + } else { // iter == this->warmup vector deals; - if (m_full_board_situation.has_value()) { + if (m_analysis.full_board.has_value()) { deals.push_back(deal); } else { deals = this->getAllAbstractionDeal(deal); @@ -869,27 +877,25 @@ void PCfrSolver::stop() { } void PCfrSolver::train(const vector& locked_nodes, const std::optional& full_board) { - this->m_locked_nodes_map.clear(); - if (full_board.has_value()) { - qDebug() << "PCfrSolver::train received full_board_situation."; - } else { - qDebug() << "PCfrSolver::train did NOT receive full_board_situation."; - } - if (!locked_nodes.empty()) { - qDebug().noquote() << "Node locking enabled for" << locked_nodes.size() << "rules."; - for (const auto& locked_node : locked_nodes) { - this->m_locked_nodes_map[locked_node.node_path] = &locked_node; - qDebug().noquote() << " - Locking node path:" << QString::fromStdString(locked_node.node_path) << "for player" << locked_node.player_to_lock; + // Reset and configure analysis state + m_analysis = AnalysisState(); + m_analysis.enabled = !locked_nodes.empty() || full_board.has_value(); + + if (m_analysis.enabled) { + if (full_board.has_value()) { + qDebug() << "PCfrSolver::train received full_board_situation. Full board analysis enabled."; + m_analysis.full_board = full_board; } - } - this->m_full_board_situation = full_board; - if (m_full_board_situation.has_value()) { - qDebug().noquote() << "Full board analysis enabled."; - if (this->warmup > 0) { - qDebug().noquote() << " - Isomorphic deal expansion will be disabled during warmup iterations."; + if (!locked_nodes.empty()) { + qDebug().noquote() << "Node locking enabled for" << locked_nodes.size() << "rules."; + for (const auto& locked_node : locked_nodes) { + m_analysis.locked_nodes_map[locked_node.node_path] = &locked_node; + qDebug().noquote() << " - Locking node path:" << QString::fromStdString(locked_node.node_path) << "for player" << locked_node.player_to_lock; + } } + } else { + qDebug() << "Analysis mode disabled."; } - vector> player_privates(this->player_number); player_privates[0] = pcm.getPreflopCards(0); player_privates[1] = pcm.getPreflopCards(1); @@ -1137,10 +1143,9 @@ ActionStrategy PCfrSolver::get_strategy(shared_ptr node,vector result.strategy_per_hand.assign(52, vector>(52, vector(result.actions.size(), 0.0f))); // Check for locked node first, and return the fixed strategy if found. - auto it = m_locked_nodes_map.find(path); - if (it != m_locked_nodes_map.end() && it->second->player_to_lock == node->getPlayer()) { - result.strategy_per_hand.assign(52, vector>(52, vector(result.actions.size(), 0.0f))); - const Strategy& locked_strategy = it->second->locked_strategy; + const Strategy* locked_strategy_ptr = m_analysis.getLockedStrategy(path, node->getPlayer()); + if (locked_strategy_ptr) { + const Strategy& locked_strategy = *locked_strategy_ptr; for (const auto& private_card : ranges[node->getPlayer()]) { vector hand_strategy(result.actions.size(), 0.0f); // Sized to full action list, init to 0 diff --git a/src/ui/treemodel.cpp b/src/ui/treemodel.cpp index ea04679a..3d28df20 100644 --- a/src/ui/treemodel.cpp +++ b/src/ui/treemodel.cpp @@ -127,55 +127,59 @@ void TreeModel::populate(const QModelIndex &index) if (!node) return; const auto& locked_nodes = this->qSolverJob->locked_nodes; - bool is_view_locked = !locked_nodes.empty(); + const auto& full_board_situation = this->qSolverJob->full_board_situation; + bool analysis_mode = full_board_situation.has_value() || !locked_nodes.empty(); if (node->getType() == GameTreeNode::GameTreeNodeType::ACTION) { shared_ptr actionNode = dynamic_pointer_cast(node); string path = item->getActionPath(); const auto& actions = actionNode->getActions(); const auto& childrens = actionNode->getChildrens(); - std::vector> children_to_add; - - if (!is_view_locked) { - // Normal mode: add all children - children_to_add = childrens; - } else { - // Locked mode: filter children based on rules - const LockedNode* lock_rule = nullptr; - for(const auto& rule : locked_nodes) { - if (rule.node_path == path && rule.player_to_lock == actionNode->getPlayer()) { - lock_rule = &rule; - break; + std::vector> children_to_add = childrens; + + if (analysis_mode) { + children_to_add.clear(); + if (!locked_nodes.empty()) { + // Locked mode: filter children based on rules + const LockedNode* lock_rule = nullptr; + for(const auto& rule : locked_nodes) { + if (rule.node_path == path && rule.player_to_lock == actionNode->getPlayer()) { + lock_rule = &rule; + break; + } } - } - for (size_t i = 0; i < actions.size(); ++i) { - bool add_child = false; - if (lock_rule) { - // A rule exists for this specific node, only add actions in the rule - Action action_key; - switch(actions[i].getAction()) { - case GameTreeNode::PokerActions::FOLD: action_key = -1; break; - case GameTreeNode::PokerActions::CHECK: case GameTreeNode::PokerActions::CALL: action_key = 0; break; - case GameTreeNode::PokerActions::BET: case GameTreeNode::PokerActions::RAISE: action_key = static_cast(actions[i].getAmount()); break; - default: continue; - } - if (lock_rule->locked_strategy.count(action_key)) { - add_child = true; - } - } else { - // No rule for this node, check if it's on a path to a future locked node - string child_path = path + actions[i].toString() + "/"; - for (const auto& rule : locked_nodes) { - if (rule.node_path.rfind(child_path, 0) == 0) { + for (size_t i = 0; i < actions.size(); ++i) { + bool add_child = false; + if (lock_rule) { + // A rule exists for this specific node, only add actions in the rule + Action action_key; + switch(actions[i].getAction()) { + case GameTreeNode::PokerActions::FOLD: action_key = -1; break; + case GameTreeNode::PokerActions::CHECK: case GameTreeNode::PokerActions::CALL: action_key = 0; break; + case GameTreeNode::PokerActions::BET: case GameTreeNode::PokerActions::RAISE: action_key = static_cast(actions[i].getAmount()); break; + default: continue; + } + if (lock_rule->locked_strategy.count(action_key)) { add_child = true; - break; + } + } else { + // No rule for this node, check if it's on a path to a future locked node + string child_path = path + actions[i].toString() + "/"; + for (const auto& rule : locked_nodes) { + if (rule.node_path.rfind(child_path, 0) == 0) { + add_child = true; + break; + } } } + if (add_child) { + children_to_add.push_back(childrens[i]); + } } - if (add_child) { - children_to_add.push_back(childrens[i]); - } + } else if (full_board_situation.has_value()) { + // Full board analysis without locks, show the whole tree + children_to_add = childrens; } }