From dfdb9678509a4bb8db654fd7ffdd2dd8eeef4a36 Mon Sep 17 00:00:00 2001 From: SH20RAJ Date: Thu, 29 Jan 2026 19:32:33 +0530 Subject: [PATCH 1/2] feat: MaxGameStringLength API (#1452) - Added MaxGameStringLength to Game class in spiel.h - Exposed in python bindings (pyspiel.cc) - Implemented for Kuhn Poker - Fixed compilation errors in spiel.h and bridge.cc (MutexLock) - Added unit test --- open_spiel/games/bridge/bridge.cc | 232 ++++++++------- open_spiel/games/kuhn_poker/kuhn_poker.cc | 163 ++++++----- open_spiel/games/kuhn_poker/kuhn_poker.h | 48 ++-- open_spiel/python/pybind11/pyspiel.cc | 1 + .../tests/max_game_string_length_test.py | 42 +++ open_spiel/spiel.h | 266 +++++++++--------- 6 files changed, 430 insertions(+), 322 deletions(-) create mode 100644 open_spiel/python/tests/max_game_string_length_test.py diff --git a/open_spiel/games/bridge/bridge.cc b/open_spiel/games/bridge/bridge.cc index 1c606d3fc1..6e0557a443 100644 --- a/open_spiel/games/bridge/bridge.cc +++ b/open_spiel/games/bridge/bridge.cc @@ -36,13 +36,13 @@ #include "open_spiel/abseil-cpp/absl/synchronization/mutex.h" #include "open_spiel/abseil-cpp/absl/types/optional.h" #include "open_spiel/abseil-cpp/absl/types/span.h" +#include "open_spiel/game_parameters.h" +#include "open_spiel/games/bridge/bridge_scoring.h" #include "open_spiel/games/bridge/double_dummy_solver/include/dll.h" #include "open_spiel/games/bridge/double_dummy_solver/src/Memory.h" #include "open_spiel/games/bridge/double_dummy_solver/src/SolverIF.h" #include "open_spiel/games/bridge/double_dummy_solver/src/TransTable.h" #include "open_spiel/games/bridge/double_dummy_solver/src/TransTableL.h" -#include "open_spiel/game_parameters.h" -#include "open_spiel/games/bridge/bridge_scoring.h" #include "open_spiel/observer.h" #include "open_spiel/spiel.h" #include "open_spiel/spiel_globals.h" @@ -90,7 +90,7 @@ const GameType kGameType{/*short_name=*/"bridge", {"num_tricks_in_observation", GameParameter(2)}, }}; -std::shared_ptr Factory(const GameParameters& params) { +std::shared_ptr Factory(const GameParameters ¶ms) { return std::shared_ptr(new BridgeGame(params)); } @@ -119,37 +119,43 @@ int Card(Suit suit, int rank) { return rank * kNumSuits + static_cast(suit); } -constexpr std::array kRankNames = { +constexpr std::array kRankNames = { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"}; -constexpr std::array kSuitNames = {"♣", "♦", "♥", "♠"}; +constexpr std::array kSuitNames = {"♣", "♦", "♥", "♠"}; std::string CardString(int card) { return absl::StrCat(kSuitNames[static_cast(CardSuit(card))], kRankNames[CardRank(card)]); } -constexpr std::array kLevelString = { +constexpr std::array kLevelString = { "-", "1", "2", "3", "4", "5", "6", "7"}; -constexpr std::array kDenominationString = { +constexpr std::array kDenominationString = { "♣", "♦", "♥", "♠", "NT"}; -constexpr std::array kDenominationStringAscii = +constexpr std::array kDenominationStringAscii = {"C", "D", "H", "S", "NT"}; -constexpr std::array kPlayerNames = {"North", "East", - "South", "West"}; +constexpr std::array kPlayerNames = { + "North", "East", "South", "West"}; std::string BidString(int bid) { - if (bid == kPass) return "Pass"; - if (bid == kDouble) return "Dbl"; - if (bid == kRedouble) return "RDbl"; + if (bid == kPass) + return "Pass"; + if (bid == kDouble) + return "Dbl"; + if (bid == kRedouble) + return "RDbl"; return absl::StrCat(kLevelString[BidLevel(bid)], kDenominationString[BidSuit(bid)]); } std::string BidStringAscii(int bid) { - if (bid == kPass) return "Pass"; - if (bid == kDouble) return "Dbl"; - if (bid == kRedouble) return "RDbl"; + if (bid == kPass) + return "Pass"; + if (bid == kDouble) + return "Dbl"; + if (bid == kRedouble) + return "RDbl"; return absl::StrCat(kLevelString[BidLevel(bid)], kDenominationStringAscii[BidSuit(bid)]); } @@ -158,9 +164,9 @@ std::string BidStringAscii(int bid) { // We call 0 and 2 partnership 0, and 1 and 3 partnership 1. int Partnership(Player player) { return player & 1; } int Partner(Player player) { return player ^ 2; } -} // namespace +} // namespace -BridgeGame::BridgeGame(const GameParameters& params) +BridgeGame::BridgeGame(const GameParameters ¶ms) : Game(kGameType, params) {} BridgeState::BridgeState(std::shared_ptr game, @@ -168,8 +174,7 @@ BridgeState::BridgeState(std::shared_ptr game, bool is_dealer_vulnerable, bool is_non_dealer_vulnerable, Player dealer, int num_tricks_in_observation) - : State(game), - use_double_dummy_result_(use_double_dummy_result), + : State(game), use_double_dummy_result_(use_double_dummy_result), dealer_(dealer), is_vulnerable_{ Partnership(dealer) ? is_non_dealer_vulnerable : is_dealer_vulnerable, @@ -189,14 +194,16 @@ std::string BridgeState::ToString() const { absl::StrCat(FormatDealer(), FormatVulnerability(), FormatDeal()); if (history_.size() > kNumCards) absl::StrAppend(&rv, FormatAuction(/*trailing_query=*/false)); - if (num_cards_played_ > 0) absl::StrAppend(&rv, "\n\n", FormatPlay()); - if (IsTerminal()) absl::StrAppend(&rv, "\n\n", FormatResult()); + if (num_cards_played_ > 0) + absl::StrAppend(&rv, "\n\n", FormatPlay()); + if (IsTerminal()) + absl::StrAppend(&rv, "\n\n", FormatResult()); return rv; } -std::array FormatHand( - int player, bool mark_voids, - const std::array, kNumCards>& deal) { +std::array +FormatHand(int player, bool mark_voids, + const std::array, kNumCards> &deal) { std::array cards; for (int suit = 0; suit < kNumSuits; ++suit) { absl::StrAppend(&cards[suit], kSuitNames[suit]); @@ -207,20 +214,24 @@ std::array FormatHand( is_void = false; } } - if (is_void && mark_voids) absl::StrAppend(&cards[suit], " none"); + if (is_void && mark_voids) + absl::StrAppend(&cards[suit], " none"); } return cards; } -std::unique_ptr BridgeState::ResampleFromInfostate( - int player_id, std::function rng) const { +std::unique_ptr +BridgeState::ResampleFromInfostate(int player_id, + std::function rng) const { // Only works in the auction phase for now. SPIEL_CHECK_TRUE(phase_ == Phase::kAuction); std::vector our_cards; std::vector other_cards; for (int i = 0; i < kNumCards; ++i) { - if (holder_[i] == player_id) our_cards.push_back(i); - else if (holder_[i].has_value()) other_cards.push_back(i); + if (holder_[i] == player_id) + our_cards.push_back(i); + else if (holder_[i].has_value()) + other_cards.push_back(i); } std::unique_ptr new_state = GetGame()->NewInitialState(); for (int i = 0; i < kNumCards; ++i) { @@ -240,12 +251,15 @@ std::unique_ptr BridgeState::ResampleFromInfostate( return new_state; } -std::string ContractString(const Contract& contract) { - if (contract.level == 0) return "Passed Out"; +std::string ContractString(const Contract &contract) { + if (contract.level == 0) + return "Passed Out"; std::string str = absl::StrCat(contract.level, kDenominationString[contract.trumps]); - if (contract.double_status == kDoubled) absl::StrAppend(&str, " Doubled"); - if (contract.double_status == kRedoubled) absl::StrAppend(&str, " Redoubled"); + if (contract.double_status == kDoubled) + absl::StrAppend(&str, " Doubled"); + if (contract.double_status == kRedoubled) + absl::StrAppend(&str, " Redoubled"); absl::StrAppend(&str, " by ", std::string{kPlayerNames[contract.declarer]}); return str; } @@ -253,7 +267,8 @@ std::string ContractString(const Contract& contract) { std::string BridgeState::InformationStateString(Player player) const { SPIEL_CHECK_GE(player, 0); SPIEL_CHECK_LT(player, num_players_); - if (IsTerminal()) return ToString(); + if (IsTerminal()) + return ToString(); std::string rv = absl::StrCat(FormatDealer(), FormatVulnerability(), "\n"); auto cards = FormatHand(player, /*mark_voids=*/true, holder_); absl::StrAppend(&rv, "You are ", kPlayerNames[player], "; you hold:\n"); @@ -308,8 +323,8 @@ std::string BridgeState::ObservationString(Player player) const { return InformationStateString(player); } -std::array, kNumCards> BridgeState::OriginalDeal() - const { +std::array, kNumCards> +BridgeState::OriginalDeal() const { SPIEL_CHECK_GE(history_.size(), kNumCards); std::array, kNumCards> deal; for (int i = 0; i < kNumCards; ++i) @@ -400,7 +415,8 @@ std::string BridgeState::FormatPlay() const { absl::StrAppend(&rv, ". Won by ", kPlayerNames[player], ".\n"); } } - if (num_cards_played_ % kNumPlayers > 0) absl::StrAppend(&rv, "\n"); + if (num_cards_played_ % kNumPlayers > 0) + absl::StrAppend(&rv, "\n"); absl::StrAppend(&rv, "\nDeclarer tricks won: ", num_declarer_tricks_); absl::StrAppend(&rv, "\nDedefence tricks won: ", num_cards_played_ / 4 - num_declarer_tricks_); @@ -425,7 +441,7 @@ void BridgeState::ObservationTensor(Player player, } void BridgeState::InformationStateTensor(Player player, - absl::Span values) const { + absl::Span values) const { SPIEL_CHECK_EQ(values.size(), game_->ObservationTensorSize()); WriteObservationTensor(player, values); } @@ -436,13 +452,15 @@ void BridgeState::WriteObservationTensor(Player player, SPIEL_CHECK_LT(player, num_players_); std::fill(values.begin(), values.end(), 0.0); - if (phase_ == Phase::kDeal) return; + if (phase_ == Phase::kDeal) + return; int partnership = Partnership(player); auto ptr = values.begin(); if (num_cards_played_ > 0) { // Observation for play phase const bool defending = (partnership != Partnership(contract_.declarer)); - if (phase_ == Phase::kPlay) ptr[2 + defending] = 1; + if (phase_ == Phase::kPlay) + ptr[2 + defending] = 1; ptr += kNumObservationTypes; // Contract @@ -468,13 +486,15 @@ void BridgeState::WriteObservationTensor(Player player, // Our remaining cards. for (int i = 0; i < kNumCards; ++i) - if (holder_[i] == player) ptr[i] = 1; + if (holder_[i] == player) + ptr[i] = 1; ptr += kNumCards; // Dummy's remaining cards. const int dummy = Partner(contract_.declarer); for (int i = 0; i < kNumCards; ++i) - if (holder_[i] == dummy) ptr[i] = 1; + if (holder_[i] == dummy) + ptr[i] = 1; ptr += kNumCards; // Indexing into history for recent tricks. @@ -538,7 +558,8 @@ void BridgeState::WriteObservationTensor(Player player, for (int i = kNumCards; i < history_.size(); ++i) { int this_call = history_[i].action - kBiddingActionBase; int relative_bidder = (i + kNumPlayers - player) % kNumPlayers; - if (last_bid == 0 && this_call == kPass) ptr[relative_bidder] = 1; + if (last_bid == 0 && this_call == kPass) + ptr[relative_bidder] = 1; if (this_call == kDouble) { ptr[kNumPlayers + (last_bid - kFirstBid) * kNumPlayers * 3 + kNumPlayers + relative_bidder] = 1; @@ -553,7 +574,8 @@ void BridgeState::WriteObservationTensor(Player player, } ptr += kNumPlayers * (1 + 3 * kNumBids); for (int i = 0; i < kNumCards; ++i) - if (holder_[i] == player) ptr[i] = 1; + if (holder_[i] == player) + ptr[i] = 1; ptr += kNumCards; SPIEL_CHECK_EQ(std::distance(values.begin(), ptr), kAuctionTensorSize + kNumObservationTypes); @@ -569,17 +591,19 @@ std::vector BridgeState::PublicObservationTensor() const { ptr += kNumVulnerabilities; ptr[is_vulnerable_[1]] = 1; ptr += kNumVulnerabilities; - auto bidding = ptr + 2 * kNumPlayers; // initial and recent passes + auto bidding = ptr + 2 * kNumPlayers; // initial and recent passes int last_bid = 0; for (int i = kNumCards; i < history_.size(); ++i) { const int player = i % kNumPlayers; const int this_call = history_[i].action - kBiddingActionBase; if (this_call == kPass) { - if (last_bid == 0) ptr[player] = 1; // Leading passes - ptr[kNumPlayers + player] = 1; // Trailing passes + if (last_bid == 0) + ptr[player] = 1; // Leading passes + ptr[kNumPlayers + player] = 1; // Trailing passes } else { // Call is a non-Pass, so clear the trailing pass markers. - for (int i = 0; i < kNumPlayers; ++i) ptr[kNumPlayers + i] = 0; + for (int i = 0; i < kNumPlayers; ++i) + ptr[kNumPlayers + i] = 0; if (this_call == kDouble) { auto base = bidding + (last_bid - kFirstBid) * kNumPlayers * 3; base[kNumPlayers + player] = 1; @@ -599,7 +623,8 @@ std::vector BridgeState::PublicObservationTensor() const { std::vector BridgeState::PrivateObservationTensor(Player player) const { std::vector rv(kNumCards); for (int i = 0; i < kNumCards; ++i) - if (holder_[i] == player) rv[i] = 1; + if (holder_[i] == player) + rv[i] = 1; return rv; } @@ -613,7 +638,7 @@ ABSL_CONST_INIT absl::Mutex dds_mutex(absl::kConstInit); void BridgeState::ComputeDoubleDummyTricks() const { if (!double_dummy_results_.has_value()) { // TODO(author11) Make DDS code thread-safe - absl::MutexLock lock(dds_mutex); + absl::MutexLock lock(&dds_mutex); double_dummy_results_ = ddTableResults{}; ddTableDeal dd_table_deal{}; for (int suit = 0; suit < kNumSuits; ++suit) { @@ -634,8 +659,9 @@ void BridgeState::ComputeDoubleDummyTricks() const { ComputeScoreByContract(); } -std::vector BridgeState::ScoreForContracts( - int player, const std::vector& contracts) const { +std::vector +BridgeState::ScoreForContracts(int player, + const std::vector &contracts) const { // Storage for the number of tricks. std::array, kNumDenominations> dd_tricks; @@ -651,23 +677,24 @@ std::vector BridgeState::ScoreForContracts( { // This performs some sort of global initialization; unclear // exactly what. - absl::MutexLock lock(dds_mutex); + absl::MutexLock lock(&dds_mutex); DDS_EXTERNAL(SetMaxThreads)(0); } // Working storage for DD calculation. auto thread_data = std::make_unique(); auto transposition_table = std::make_unique(); - transposition_table->SetMemoryDefault(95); // megabytes - transposition_table->SetMemoryMaximum(160); // megabytes + transposition_table->SetMemoryDefault(95); // megabytes + transposition_table->SetMemoryMaximum(160); // megabytes transposition_table->MakeTT(); thread_data->transTable = transposition_table.get(); // Which trump suits do we need to handle? std::set suits; for (auto index : contracts) { - const auto& contract = kAllContracts[index]; - if (contract.level > 0) suits.emplace(contract.trumps); + const auto &contract = kAllContracts[index]; + if (contract.level > 0) + suits.emplace(contract.trumps); } // Build the deal ::deal dl{}; @@ -690,7 +717,7 @@ std::vector BridgeState::ScoreForContracts( // Assemble the declarers we need to consider. std::set declarers; for (auto index : contracts) { - const auto& contract = kAllContracts[index]; + const auto &contract = kAllContracts[index]; if (contract.level > 0 && contract.trumps == suit) declarers.emplace(contract.declarer); } @@ -705,10 +732,10 @@ std::vector BridgeState::ScoreForContracts( // First time we're calculating this trump suit. const int return_code = SolveBoardInternal( thread_data.get(), dl, - /*target=*/-1, // Find max number of tricks - /*solutions=*/1, // Just the tricks (no card-by-card result) - /*mode=*/2, // Unclear - &fut // Output + /*target=*/-1, // Find max number of tricks + /*solutions=*/1, // Just the tricks (no card-by-card result) + /*mode=*/2, // Unclear + &fut // Output ); if (return_code != RETURN_NO_FAULT) { char error_message[80]; @@ -742,7 +769,7 @@ std::vector BridgeState::ScoreForContracts( std::vector scores; scores.reserve(contracts.size()); for (int contract_index : contracts) { - const Contract& contract = kAllContracts[contract_index]; + const Contract &contract = kAllContracts[contract_index]; const int declarer_score = (contract.level == 0) ? 0 @@ -757,14 +784,14 @@ std::vector BridgeState::ScoreForContracts( std::vector BridgeState::LegalActions() const { switch (phase_) { - case Phase::kDeal: - return DealLegalActions(); - case Phase::kAuction: - return BiddingLegalActions(); - case Phase::kPlay: - return PlayLegalActions(); - default: - return {}; + case Phase::kDeal: + return DealLegalActions(); + case Phase::kAuction: + return BiddingLegalActions(); + case Phase::kPlay: + return PlayLegalActions(); + default: + return {}; } } @@ -772,7 +799,8 @@ std::vector BridgeState::DealLegalActions() const { std::vector legal_actions; legal_actions.reserve(kNumCards - history_.size()); for (int i = 0; i < kNumCards; ++i) { - if (!holder_[i].has_value()) legal_actions.push_back(i); + if (!holder_[i].has_value()) + legal_actions.push_back(i); } return legal_actions; } @@ -811,11 +839,13 @@ std::vector BridgeState::PlayLegalActions() const { } } } - if (!legal_actions.empty()) return legal_actions; + if (!legal_actions.empty()) + return legal_actions; // Otherwise, we can play any of our cards. for (int card = 0; card < kNumCards; ++card) { - if (holder_[card] == current_player_) legal_actions.push_back(card); + if (holder_[card] == current_player_) + legal_actions.push_back(card); } return legal_actions; } @@ -826,28 +856,30 @@ std::vector> BridgeState::ChanceOutcomes() const { outcomes.reserve(num_cards_remaining); const double p = 1.0 / static_cast(num_cards_remaining); for (int card = 0; card < kNumCards; ++card) { - if (!holder_[card].has_value()) outcomes.emplace_back(card, p); + if (!holder_[card].has_value()) + outcomes.emplace_back(card, p); } return outcomes; } void BridgeState::DoApplyAction(Action action) { switch (phase_) { - case Phase::kDeal: - return ApplyDealAction(action); - case Phase::kAuction: - return ApplyBiddingAction(action - kBiddingActionBase); - case Phase::kPlay: - return ApplyPlayAction(action); - case Phase::kGameOver: - SpielFatalError("Cannot act in terminal states"); + case Phase::kDeal: + return ApplyDealAction(action); + case Phase::kAuction: + return ApplyBiddingAction(action - kBiddingActionBase); + case Phase::kPlay: + return ApplyPlayAction(action); + case Phase::kGameOver: + SpielFatalError("Cannot act in terminal states"); } } void BridgeState::ApplyDealAction(int card) { holder_[card] = (history_.size() % kNumPlayers); if (history_.size() == kNumCards - 1) { - if (use_double_dummy_result_) ComputeDoubleDummyTricks(); + if (use_double_dummy_result_) + ComputeDoubleDummyTricks(); phase_ = Phase::kAuction; current_player_ = dealer_; } @@ -1006,12 +1038,8 @@ void BridgeState::ComputeScoreByContract() const { } Trick::Trick(Player leader, Denomination trumps, int card) - : trumps_(trumps), - led_suit_(CardSuit(card)), - winning_suit_(CardSuit(card)), - winning_rank_(CardRank(card)), - leader_(leader), - winning_player_(leader) {} + : trumps_(trumps), led_suit_(CardSuit(card)), winning_suit_(CardSuit(card)), + winning_rank_(CardRank(card)), leader_(leader), winning_player_(leader) {} void Trick::Play(Player player, int card) { if (CardSuit(card) == winning_suit_) { @@ -1043,9 +1071,10 @@ std::string BridgeState::Serialize() const { return serialized; } -std::unique_ptr BridgeGame::DeserializeState( - const std::string& str) const { - if (!UseDoubleDummyResult()) return Game::DeserializeState(str); +std::unique_ptr +BridgeGame::DeserializeState(const std::string &str) const { + if (!UseDoubleDummyResult()) + return Game::DeserializeState(str); auto state = std::make_unique( shared_from_this(), UseDoubleDummyResult(), IsDealerVulnerable(), IsNonDealerVulnerable(), Dealer(), NumTricksInObservation()); @@ -1057,7 +1086,8 @@ std::unique_ptr BridgeGame::DeserializeState( auto it = separator; int i = 0; while (++it != lines.end()) { - if (it->empty()) continue; + if (it->empty()) + continue; double_dummy_results.resTable[i / kNumPlayers][i % kNumPlayers] = std::stol(*it); ++i; @@ -1066,7 +1096,8 @@ std::unique_ptr BridgeGame::DeserializeState( } // Actions in the game. for (auto it = lines.begin(); it != separator; ++it) { - if (it->empty()) continue; + if (it->empty()) + continue; state->ApplyAction(std::stol(*it)); } return state; @@ -1081,8 +1112,9 @@ std::string BridgeGame::ContractString(int index) const { return kAllContracts[index].ToString(); } -std::unique_ptr BridgeGame::NewDuplicateBridgeInitialState( - int tournament_seed, int board_number) const { +std::unique_ptr +BridgeGame::NewDuplicateBridgeInitialState(int tournament_seed, + int board_number) const { // Standard assignments of dealer and vulnerability based on the board number. const Player dealer = (board_number - 1) % kNumPlayers; const bool @@ -1119,5 +1151,5 @@ std::unique_ptr BridgeGame::NewDuplicateBridgeInitialState( return state; } -} // namespace bridge -} // namespace open_spiel +} // namespace bridge +} // namespace open_spiel diff --git a/open_spiel/games/kuhn_poker/kuhn_poker.cc b/open_spiel/games/kuhn_poker/kuhn_poker.cc index 566268d08a..3d8de36d28 100644 --- a/open_spiel/games/kuhn_poker/kuhn_poker.cc +++ b/open_spiel/games/kuhn_poker/kuhn_poker.cc @@ -35,55 +35,56 @@ constexpr int kDefaultPlayers = 2; constexpr double kAnte = 1; // Facts about the game -const GameType kGameType{/*short_name=*/"kuhn_poker", - /*long_name=*/"Kuhn Poker", - GameType::Dynamics::kSequential, - GameType::ChanceMode::kExplicitStochastic, - GameType::Information::kImperfectInformation, - GameType::Utility::kZeroSum, - GameType::RewardModel::kTerminal, - /*max_num_players=*/10, - /*min_num_players=*/2, - /*provides_information_state_string=*/true, - /*provides_information_state_tensor=*/true, - /*provides_observation_string=*/true, - /*provides_observation_tensor=*/true, - /*parameter_specification=*/ - {{"players", GameParameter(kDefaultPlayers)}}, - /*default_loadable=*/true, - /*provides_factored_observation_string=*/true, - }; - -std::shared_ptr Factory(const GameParameters& params) { +const GameType kGameType{ + /*short_name=*/"kuhn_poker", + /*long_name=*/"Kuhn Poker", + GameType::Dynamics::kSequential, + GameType::ChanceMode::kExplicitStochastic, + GameType::Information::kImperfectInformation, + GameType::Utility::kZeroSum, + GameType::RewardModel::kTerminal, + /*max_num_players=*/10, + /*min_num_players=*/2, + /*provides_information_state_string=*/true, + /*provides_information_state_tensor=*/true, + /*provides_observation_string=*/true, + /*provides_observation_tensor=*/true, + /*parameter_specification=*/ + {{"players", GameParameter(kDefaultPlayers)}}, + /*default_loadable=*/true, + /*provides_factored_observation_string=*/true, +}; + +std::shared_ptr Factory(const GameParameters ¶ms) { return std::shared_ptr(new KuhnGame(params)); } REGISTER_SPIEL_GAME(kGameType, Factory); open_spiel::RegisterSingleTensorObserver single_tensor(kGameType.short_name); -} // namespace +} // namespace class KuhnObserver : public Observer { - public: +public: KuhnObserver(IIGObservationType iig_obs_type) : Observer(/*has_string=*/true, /*has_tensor=*/true), iig_obs_type_(iig_obs_type) {} - void WriteTensor(const State& observed_state, int player, - Allocator* allocator) const override { - const KuhnState& state = - open_spiel::down_cast(observed_state); + void WriteTensor(const State &observed_state, int player, + Allocator *allocator) const override { + const KuhnState &state = + open_spiel::down_cast(observed_state); SPIEL_CHECK_GE(player, 0); SPIEL_CHECK_LT(player, state.num_players_); const int num_players = state.num_players_; const int num_cards = num_players + 1; if (iig_obs_type_.private_info == PrivateInfoType::kSinglePlayer) { - { // Observing player. + { // Observing player. auto out = allocator->Get("player", {num_players}); out.at(player) = 1; } - { // The player's card, if one has been dealt. + { // The player's card, if one has been dealt. auto out = allocator->Get("private_card", {num_cards}); if (state.history_.size() > player) out.at(state.history_[player].action) = 1; @@ -106,10 +107,10 @@ class KuhnObserver : public Observer { } } - std::string StringFrom(const State& observed_state, + std::string StringFrom(const State &observed_state, int player) const override { - const KuhnState& state = - open_spiel::down_cast(observed_state); + const KuhnState &state = + open_spiel::down_cast(observed_state); SPIEL_CHECK_GE(player, 0); SPIEL_CHECK_LT(player, state.num_players_); std::string result; @@ -165,16 +166,14 @@ class KuhnObserver : public Observer { return result; } - private: +private: IIGObservationType iig_obs_type_; }; KuhnState::KuhnState(std::shared_ptr game) - : State(game), - first_bettor_(kInvalidPlayer), + : State(game), first_bettor_(kInvalidPlayer), card_dealt_(game->NumPlayers() + 1, kInvalidPlayer), - winner_(kInvalidPlayer), - pot_(kAnte * game->NumPlayers()), + winner_(kInvalidPlayer), pot_(kAnte * game->NumPlayers()), // How much each player has contributed to the pot, indexed by pid. ante_(game->NumPlayers(), kAnte) {} @@ -194,7 +193,8 @@ void KuhnState::DoApplyAction(Action move) { // kChancePlayerId, so we use that instead). card_dealt_[move] = history_.size(); } else if (move == ActionType::kBet) { - if (first_bettor_ == kInvalidPlayer) first_bettor_ = CurrentPlayer(); + if (first_bettor_ == kInvalidPlayer) + first_bettor_ = CurrentPlayer(); pot_ += 1; ante_[CurrentPlayer()] += kAnte; } @@ -210,7 +210,8 @@ void KuhnState::DoApplyAction(Action move) { // which is either the highest or the next-highest card. // Losers lose 1, winner wins 1 * (num_players - 1) winner_ = card_dealt_[num_players_]; - if (winner_ == kInvalidPlayer) winner_ = card_dealt_[num_players_ - 1]; + if (winner_ == kInvalidPlayer) + winner_ = card_dealt_[num_players_ - 1]; } else if (first_bettor_ != kInvalidPlayer && num_actions == num_players_ + first_bettor_) { // There was betting; so the winner is the person with the highest card @@ -229,11 +230,13 @@ void KuhnState::DoApplyAction(Action move) { } std::vector KuhnState::LegalActions() const { - if (IsTerminal()) return {}; + if (IsTerminal()) + return {}; if (IsChanceNode()) { std::vector actions; for (int card = 0; card < card_dealt_.size(); ++card) { - if (card_dealt_[card] == kInvalidPlayer) actions.push_back(card); + if (card_dealt_[card] == kInvalidPlayer) + actions.push_back(card); } return actions; } else { @@ -254,12 +257,14 @@ std::string KuhnState::ToString() const { // The deal: space separated card per player std::string str; for (int i = 0; i < history_.size() && i < num_players_; ++i) { - if (!str.empty()) str.push_back(' '); + if (!str.empty()) + str.push_back(' '); absl::StrAppend(&str, history_[i].action); } // The betting history: p for Pass, b for Bet - if (history_.size() > num_players_) str.push_back(' '); + if (history_.size() > num_players_) + str.push_back(' '); for (int i = num_players_; i < history_.size(); ++i) { str.push_back(history_[i].action ? 'b' : 'p'); } @@ -283,26 +288,26 @@ std::vector KuhnState::Returns() const { } std::string KuhnState::InformationStateString(Player player) const { - const KuhnGame& game = open_spiel::down_cast(*game_); + const KuhnGame &game = open_spiel::down_cast(*game_); return game.info_state_observer_->StringFrom(*this, player); } std::string KuhnState::ObservationString(Player player) const { - const KuhnGame& game = open_spiel::down_cast(*game_); + const KuhnGame &game = open_spiel::down_cast(*game_); return game.default_observer_->StringFrom(*this, player); } void KuhnState::InformationStateTensor(Player player, absl::Span values) const { ContiguousAllocator allocator(values); - const KuhnGame& game = open_spiel::down_cast(*game_); + const KuhnGame &game = open_spiel::down_cast(*game_); game.info_state_observer_->WriteTensor(*this, player, &allocator); } void KuhnState::ObservationTensor(Player player, absl::Span values) const { ContiguousAllocator allocator(values); - const KuhnGame& game = open_spiel::down_cast(*game_); + const KuhnGame &game = open_spiel::down_cast(*game_); game.default_observer_->WriteTensor(*this, player, &allocator); } @@ -318,7 +323,8 @@ void KuhnState::UndoAction(Player player, Action move) { // Undoing a bet / pass. if (move == ActionType::kBet) { pot_ -= 1; - if (player == first_bettor_) first_bettor_ = kInvalidPlayer; + if (player == first_bettor_) + first_bettor_ = kInvalidPlayer; } winner_ = kInvalidPlayer; } @@ -331,7 +337,8 @@ std::vector> KuhnState::ChanceOutcomes() const { std::vector> outcomes; const double p = 1.0 / (num_players_ + 1 - history_.size()); for (int card = 0; card < card_dealt_.size(); ++card) { - if (card_dealt_[card] == kInvalidPlayer) outcomes.push_back({card, p}); + if (card_dealt_[card] == kInvalidPlayer) + outcomes.push_back({card, p}); } return outcomes; } @@ -348,12 +355,14 @@ bool KuhnState::DidBet(Player player) const { } } -std::unique_ptr KuhnState::ResampleFromInfostate( - int player_id, std::function rng) const { +std::unique_ptr +KuhnState::ResampleFromInfostate(int player_id, + std::function rng) const { std::unique_ptr state = game_->NewInitialState(); Action player_chance = history_.at(player_id).action; for (int p = 0; p < game_->NumPlayers(); ++p) { - if (p == history_.size()) return state; + if (p == history_.size()) + return state; if (p == player_id) { state->ApplyAction(player_chance); } else { @@ -365,27 +374,28 @@ std::unique_ptr KuhnState::ResampleFromInfostate( } } SPIEL_CHECK_GE(state->CurrentPlayer(), 0); - if (game_->NumPlayers() == history_.size()) return state; + if (game_->NumPlayers() == history_.size()) + return state; for (int i = game_->NumPlayers(); i < history_.size(); ++i) { state->ApplyAction(history_.at(i).action); } return state; } -KuhnGame::KuhnGame(const GameParameters& params) +KuhnGame::KuhnGame(const GameParameters ¶ms) : Game(kGameType, params), num_players_(ParameterValue("players")) { SPIEL_CHECK_GE(num_players_, kGameType.min_num_players); SPIEL_CHECK_LE(num_players_, kGameType.max_num_players); default_observer_ = std::make_shared(kDefaultObsType); info_state_observer_ = std::make_shared(kInfoStateObsType); private_observer_ = std::make_shared( - IIGObservationType{/*public_info*/false, - /*perfect_recall*/false, - /*private_info*/PrivateInfoType::kSinglePlayer}); + IIGObservationType{/*public_info*/ false, + /*perfect_recall*/ false, + /*private_info*/ PrivateInfoType::kSinglePlayer}); public_observer_ = std::make_shared( - IIGObservationType{/*public_info*/true, - /*perfect_recall*/false, - /*private_info*/PrivateInfoType::kNone}); + IIGObservationType{/*public_info*/ true, + /*perfect_recall*/ false, + /*private_info*/ PrivateInfoType::kNone}); } std::unique_ptr KuhnGame::NewInitialState() const { @@ -409,6 +419,19 @@ std::vector KuhnGame::ObservationTensorShape() const { return {3 * num_players_ + 1}; } +int KuhnGame::MaxGameStringLength() const { + // Serialize format: "card1 card2 ... cardN bpb..." + // Cards: num_players_ integers. Max value is num_players_. + // Length of one card: string length of num_players_. + // Num spaces between cards: num_players_ - 1. + // Separator between cards and betting: 1 space. + // Betting history: MaxGameLength() characters (p/b). + int card_len = std::to_string(num_players_).length(); + int cards_str_len = num_players_ * card_len + (num_players_ - 1); + int betting_str_len = MaxGameLength(); + return cards_str_len + 1 + betting_str_len; +} + double KuhnGame::MaxUtility() const { // In poker, the utility is defined as the money a player has at the end // of the game minus then money the player had before starting the game. @@ -425,9 +448,9 @@ double KuhnGame::MinUtility() const { return -2; } -std::shared_ptr KuhnGame::MakeObserver( - absl::optional iig_obs_type, - const GameParameters& params) const { +std::shared_ptr +KuhnGame::MakeObserver(absl::optional iig_obs_type, + const GameParameters ¶ms) const { if (params.empty()) { return std::make_shared( iig_obs_type.value_or(kDefaultObsType)); @@ -436,15 +459,15 @@ std::shared_ptr KuhnGame::MakeObserver( } } -TabularPolicy GetAlwaysPassPolicy(const Game& game) { - SPIEL_CHECK_TRUE( - dynamic_cast(const_cast(&game)) != nullptr); +TabularPolicy GetAlwaysPassPolicy(const Game &game) { + SPIEL_CHECK_TRUE(dynamic_cast(const_cast(&game)) != + nullptr); return GetPrefActionPolicy(game, {ActionType::kPass}); } -TabularPolicy GetAlwaysBetPolicy(const Game& game) { - SPIEL_CHECK_TRUE( - dynamic_cast(const_cast(&game)) != nullptr); +TabularPolicy GetAlwaysBetPolicy(const Game &game) { + SPIEL_CHECK_TRUE(dynamic_cast(const_cast(&game)) != + nullptr); return GetPrefActionPolicy(game, {ActionType::kBet}); } @@ -473,5 +496,5 @@ TabularPolicy GetOptimalPolicy(double alpha) { return TabularPolicy(policy); } -} // namespace kuhn_poker -} // namespace open_spiel +} // namespace kuhn_poker +} // namespace open_spiel diff --git a/open_spiel/games/kuhn_poker/kuhn_poker.h b/open_spiel/games/kuhn_poker/kuhn_poker.h index 7843efca87..c374c0ab41 100644 --- a/open_spiel/games/kuhn_poker/kuhn_poker.h +++ b/open_spiel/games/kuhn_poker/kuhn_poker.h @@ -48,9 +48,9 @@ class KuhnGame; class KuhnObserver; class KuhnState : public State { - public: +public: explicit KuhnState(std::shared_ptr game); - KuhnState(const KuhnState&) = default; + KuhnState(const KuhnState &) = default; Player CurrentPlayer() const override; @@ -69,15 +69,16 @@ class KuhnState : public State { std::vector> ChanceOutcomes() const override; std::vector LegalActions() const override; std::vector hand() const { return {card_dealt_[CurrentPlayer()]}; } - std::unique_ptr ResampleFromInfostate( - int player_id, std::function rng) const override; + std::unique_ptr + ResampleFromInfostate(int player_id, + std::function rng) const override; - const std::vector& CardDealt() const { return card_dealt_; } + const std::vector &CardDealt() const { return card_dealt_; } - protected: +protected: void DoApplyAction(Action move) override; - private: +private: friend class KuhnObserver; // Whether the specified player made a bet @@ -88,18 +89,18 @@ class KuhnState : public State { // extracting legal actions and utilities easier. // The cost of the additional book-keeping is more complex ApplyAction() and // UndoAction() functions. - int first_bettor_; // the player (if any) who was first to bet - std::vector card_dealt_; // the player (if any) who has each card - int winner_; // winning player, or kInvalidPlayer if the - // game isn't over yet. - int pot_; // the size of the pot + int first_bettor_; // the player (if any) who was first to bet + std::vector card_dealt_; // the player (if any) who has each card + int winner_; // winning player, or kInvalidPlayer if the + // game isn't over yet. + int pot_; // the size of the pot // How much each player has contributed to the pot, indexed by pid. std::vector ante_; }; class KuhnGame : public Game { - public: - explicit KuhnGame(const GameParameters& params); +public: + explicit KuhnGame(const GameParameters ¶ms); int NumDistinctActions() const override { return 2; } std::unique_ptr NewInitialState() const override; int MaxChanceOutcomes() const override { return num_players_ + 1; } @@ -110,10 +111,11 @@ class KuhnGame : public Game { std::vector InformationStateTensorShape() const override; std::vector ObservationTensorShape() const override; int MaxGameLength() const override { return num_players_ * 2 - 1; } + int MaxGameStringLength() const override; int MaxChanceNodesInHistory() const override { return num_players_; } - std::shared_ptr MakeObserver( - absl::optional iig_obs_type, - const GameParameters& params) const override; + std::shared_ptr + MakeObserver(absl::optional iig_obs_type, + const GameParameters ¶ms) const override; // Used to implement the old observation API. std::shared_ptr default_observer_; @@ -121,22 +123,22 @@ class KuhnGame : public Game { std::shared_ptr public_observer_; std::shared_ptr private_observer_; - private: +private: // Number of players. int num_players_; }; // Returns policy that always passes. -TabularPolicy GetAlwaysPassPolicy(const Game& game); +TabularPolicy GetAlwaysPassPolicy(const Game &game); // Returns policy that always bets. -TabularPolicy GetAlwaysBetPolicy(const Game& game); +TabularPolicy GetAlwaysBetPolicy(const Game &game); // The optimal Kuhn policy as stated at https://en.wikipedia.org/wiki/Kuhn_poker // The Nash equilibrium is parametrized by alpha \in [0, 1/3]. TabularPolicy GetOptimalPolicy(double alpha); -} // namespace kuhn_poker -} // namespace open_spiel +} // namespace kuhn_poker +} // namespace open_spiel -#endif // OPEN_SPIEL_GAMES_KUHN_POKER_H_ +#endif // OPEN_SPIEL_GAMES_KUHN_POKER_H_ diff --git a/open_spiel/python/pybind11/pyspiel.cc b/open_spiel/python/pybind11/pyspiel.cc index ed552a5f07..83ce71943e 100644 --- a/open_spiel/python/pybind11/pyspiel.cc +++ b/open_spiel/python/pybind11/pyspiel.cc @@ -460,6 +460,7 @@ PYBIND11_MODULE(pyspiel, m) { .def("max_move_number", &Game::MaxMoveNumber) .def("max_history_length", &Game::MaxHistoryLength) .def("serialize", &Game::Serialize) + .def("max_game_string_length", &Game::MaxGameStringLength) .def("to_string", &Game::ToString) .def("make_observer", [](std::shared_ptr game, IIGObservationType iig_obs_type, diff --git a/open_spiel/python/tests/max_game_string_length_test.py b/open_spiel/python/tests/max_game_string_length_test.py new file mode 100644 index 0000000000..d29c873c07 --- /dev/null +++ b/open_spiel/python/tests/max_game_string_length_test.py @@ -0,0 +1,42 @@ +# Copyright 2019 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Game::MaxGameStringLength.""" + +import unittest +import pyspiel + +class MaxGameStringLengthTest(unittest.TestCase): + + def test_kuhn_poker(self): + # 2 players: "0 1 pbp" -> 7 chars + game = pyspiel.load_game("kuhn_poker", {"players": 2}) + self.assertEqual(game.max_game_string_length(), 7) + + # 3 players: "0 1 2 pbppb" -> 3 + 2 (spaces) + 5 (bets) + 1 (space) = 11? + # Logic in cc: + # int card_len = std::to_string(num_players_).length(); // 1 for n=3 + # int cards_str_len = num_players_ * card_len + (num_players_ - 1); // 3*1 + 2 = 5 ("0 1 2") + # int betting_str_len = MaxGameLength(); // 3*2 - 1 = 5 + # total = cards_str_len + 1 + betting_str_len = 5 + 1 + 5 = 11. + game3 = pyspiel.load_game("kuhn_poker", {"players": 3}) + self.assertEqual(game3.max_game_string_length(), 11) + + def test_unimplemented_game(self): + # Default implementation returns 0. + game = pyspiel.load_game("tic_tac_toe") + self.assertEqual(game.max_game_string_length(), 0) + +if __name__ == "__main__": + unittest.main() diff --git a/open_spiel/spiel.h b/open_spiel/spiel.h index 5ab70dad1f..fc2caf01d4 100644 --- a/open_spiel/spiel.h +++ b/open_spiel/spiel.h @@ -28,12 +28,12 @@ #include "open_spiel/abseil-cpp/absl/synchronization/mutex.h" #include "open_spiel/abseil-cpp/absl/types/optional.h" #include "open_spiel/abseil-cpp/absl/types/span.h" -#include "open_spiel/json/include/nlohmann/json.hpp" -#include "open_spiel/utils/nlohmann_json.h" #include "open_spiel/game_parameters.h" +#include "open_spiel/json/include/nlohmann/json.hpp" #include "open_spiel/observer.h" #include "open_spiel/spiel_globals.h" #include "open_spiel/spiel_utils.h" +#include "open_spiel/utils/nlohmann_json.h" namespace open_spiel { @@ -54,11 +54,11 @@ struct GameType { // Is the game one-player-at-a-time or do players act simultaneously? enum class Dynamics { - kSimultaneous, // In some or all nodes every player acts. - kSequential, // Turn-based games. + kSimultaneous, // In some or all nodes every player acts. + kSequential, // Turn-based games. // Mean field game. In particular, this adds mean field nodes. Support for // mean field games is experimental. See details in games/mfg/README.md. - kMeanField, // Is a Mean Field Game + kMeanField, // Is a Mean Field Game }; Dynamics dynamics; @@ -73,28 +73,28 @@ struct GameType { // outcome). For more discussion of this field, see the github issue: // https://github.com/deepmind/open_spiel/issues/792. enum class ChanceMode { - kDeterministic, // No chance nodes - kExplicitStochastic, // Has at least one chance node, all with - // deterministic ApplyAction() - kSampledStochastic, // At least one chance node with non-deterministic - // ApplyAction() + kDeterministic, // No chance nodes + kExplicitStochastic, // Has at least one chance node, all with + // deterministic ApplyAction() + kSampledStochastic, // At least one chance node with non-deterministic + // ApplyAction() }; ChanceMode chance_mode; // The information type of the game. enum class Information { - kOneShot, // aka Normal-form games (single simultaneous turn). - kPerfectInformation, // All players know the state of the game. - kImperfectInformation, // Some information is hidden from some players. + kOneShot, // aka Normal-form games (single simultaneous turn). + kPerfectInformation, // All players know the state of the game. + kImperfectInformation, // Some information is hidden from some players. }; Information information; // Whether the game has any constraints on the player utilities. enum class Utility { - kZeroSum, // Utilities of all players sum to 0 - kConstantSum, // Utilities of all players sum to a constant - kGeneralSum, // Total utility of all players differs in different outcomes - kIdentical, // Every player gets an identical value (cooperative game). + kZeroSum, // Utilities of all players sum to 0 + kConstantSum, // Utilities of all players sum to a constant + kGeneralSum, // Total utility of all players differs in different outcomes + kIdentical, // Every player gets an identical value (cooperative game). }; Utility utility; @@ -102,8 +102,8 @@ struct GameType { // utilities at terminal states, the default implementation of State::Rewards // should work for RL uses (giving 0 everywhere except terminal states). enum class RewardModel { - kRewards, // RL-style func r(s, a, s') via State::Rewards() call at s'. - kTerminal, // Games-style, only at terminals. Call (State::Returns()). + kRewards, // RL-style func r(s, a, s') via State::Rewards() call at s'. + kTerminal, // Games-style, only at terminals. Call (State::Returns()). }; RewardModel reward_model; @@ -141,12 +141,11 @@ struct GameType { bool provides_factored_observation_string = false; bool provides_information_state() const { - return provides_information_state_tensor - || provides_information_state_string; + return provides_information_state_tensor || + provides_information_state_string; } bool provides_observation() const { - return provides_observation_tensor - || provides_observation_string; + return provides_observation_tensor || provides_observation_string; } // Is this a concrete game, i.e. an actual game? Most games in OpenSpiel are @@ -190,13 +189,13 @@ struct GameInfo { int max_game_length; }; -std::ostream& operator<<(std::ostream& os, const StateType& type); +std::ostream &operator<<(std::ostream &os, const StateType &type); -std::ostream& operator<<(std::ostream& stream, GameType::Dynamics value); -std::ostream& operator<<(std::ostream& stream, GameType::ChanceMode value); -std::ostream& operator<<(std::ostream& stream, GameType::Information value); -std::ostream& operator<<(std::ostream& stream, GameType::Utility value); -std::ostream& operator<<(std::ostream& stream, GameType::RewardModel value); +std::ostream &operator<<(std::ostream &stream, GameType::Dynamics value); +std::ostream &operator<<(std::ostream &stream, GameType::ChanceMode value); +std::ostream &operator<<(std::ostream &stream, GameType::Information value); +std::ostream &operator<<(std::ostream &stream, GameType::Utility value); +std::ostream &operator<<(std::ostream &stream, GameType::RewardModel value); // The probability of taking each possible action in a particular info state. using ActionsAndProbs = std::vector>; @@ -238,7 +237,7 @@ struct ActionStruct : public GameStruct {}; // An abstract class that represents a state of the game. class State { - public: +public: virtual ~State() = default; // Derived classes must call one of these constructors. Note that a state must @@ -250,7 +249,7 @@ class State { // be used as value types and should always be created via a shared pointer. // See the documentation of the Game object for further details. State(std::shared_ptr game); - State(const State&) = default; + State(const State &) = default; // Returns current player. Player numbers start from 0. // Negative numbers are for chance (-1) or simultaneous (-2). @@ -276,7 +275,7 @@ class State { // The default implementation will fatal error. Games that support this // should override it, map the struct to an integer action id, and then call // ApplyAction. - virtual void ApplyActionStruct(const ActionStruct& action_struct); + virtual void ApplyActionStruct(const ActionStruct &action_struct); // `LegalActions(Player player)` is valid for all nodes in all games, // returning an empty list for players who don't act at this state. The @@ -336,21 +335,21 @@ class State { // Note: This currently just loops over all legal actions, converts them into // a string, and checks equality, so it can be very slow. virtual Action StringToAction(Player player, - const std::string& action_str) const; - Action StringToAction(const std::string& action_str) const { + const std::string &action_str) const; + Action StringToAction(const std::string &action_str) const { return StringToAction(CurrentPlayer(), action_str); } // Converts an action to a structured format. - virtual std::unique_ptr ActionToStruct( - Player player, Action action_id) const { + virtual std::unique_ptr ActionToStruct(Player player, + Action action_id) const { SpielFatalError("ActionToStruct not implemented."); } std::unique_ptr ActionToStruct(Action action_id) const { return ActionToStruct(CurrentPlayer(), action_id); } - virtual Action StructToAction(const ActionStruct& action_struct) const { + virtual Action StructToAction(const ActionStruct &action_struct) const { SpielFatalError("StructToAction not implemented."); } @@ -364,7 +363,7 @@ class State { // history might be relevant for distinguishing states whereas it might not be // relevant for single-player games or perfect information games such as // Tic-Tac-Toe, where only the current board state is necessary. - virtual bool operator==(const State& other) const { + virtual bool operator==(const State &other) const { return ToString() == other.ToString(); } @@ -375,9 +374,7 @@ class State { } // Returns a JSON string representation of the state. - std::string ToJson() const { - return ToStruct()->ToJson(); - } + std::string ToJson() const { return ToStruct()->ToJson(); } // Is this a terminal state? (i.e. has the game ended?) virtual bool IsTerminal() const = 0; @@ -458,7 +455,7 @@ class State { struct PlayerAction { Player player; Action action; - bool operator==(const PlayerAction&) const; + bool operator==(const PlayerAction &) const; }; // For backward-compatibility reasons, this is the history of actions only. @@ -466,12 +463,13 @@ class State { std::vector History() const { std::vector history; history.reserve(history_.size()); - for (auto& h : history_) history.push_back(h.action); + for (auto &h : history_) + history.push_back(h.action); return history; } // The full (player, action) history. - const std::vector& FullHistory() const { return history_; } + const std::vector &FullHistory() const { return history_; } // A string representation for the history. There should be a one to one // mapping between histories (i.e. sequences of actions for all players, @@ -574,7 +572,7 @@ class State { return InformationStateTensor(CurrentPlayer()); } virtual void InformationStateTensor(Player player, - std::vector* values) const; + std::vector *values) const; // We have functions for observations which are parallel to those for // information states. An observation should have the following properties: @@ -625,15 +623,15 @@ class State { std::vector ObservationTensor() const { return ObservationTensor(CurrentPlayer()); } - void ObservationTensor(Player player, std::vector* values) const; + void ObservationTensor(Player player, std::vector *values) const; // Returns a structured representation of an observation for `player`. // // Implementations should start with (and it's tested in api_test.py): // SPIEL_CHECK_GE(player, 0); // SPIEL_CHECK_LT(player, num_players_); - virtual std::unique_ptr ToObservationStruct( - Player player) const { + virtual std::unique_ptr + ToObservationStruct(Player player) const { SpielFatalError("ToObservationStruct not implemented!"); } std::unique_ptr ToObservationStruct() const { @@ -667,11 +665,10 @@ class State { // actions at this node, then 0 should be passed instead. // // Simultaneous games should implement DoApplyActions. - void ApplyActions(const std::vector& actions); + void ApplyActions(const std::vector &actions); // A helper version of ApplyActions that first does legality checks. - void ApplyActionsWithLegalityChecks(const std::vector& actions); - + void ApplyActionsWithLegalityChecks(const std::vector &actions); // The size of the action space. See `Game` for a full description. int NumDistinctActions() const { return num_distinct_actions_; } @@ -704,7 +701,7 @@ class State { ActionsAndProbs outcomes_with_probs = ChanceOutcomes(); std::vector outcome_list; outcome_list.reserve(outcomes_with_probs.size()); - for (auto& pair : outcomes_with_probs) { + for (auto &pair : outcomes_with_probs) { outcome_list.push_back(pair.first); } return outcome_list; @@ -714,6 +711,15 @@ class State { // Decision. See StateType definition for definitions of the different types. StateType GetType() const; + // Serializes a state into a string. + // + // The default implementation writes out a sequence of actions, one per line, + // taken from the initial state. Note: this default serialization scheme will + // not work games whose chance mode is kSampledStochastic, as there is + // currently no general way to set the state's seed to ensure that it samples + // the same chance event at chance nodes. + // + // If overridden, this must be the inverse of Game::DeserializeState. // Serializes a state into a string. // // The default implementation writes out a sequence of actions, one per line, @@ -738,8 +744,8 @@ class State { // // Default implementation checks if the game is a perfect information game. // If so, it returns a clone, otherwise an error is thrown. - virtual std::unique_ptr ResampleFromInfostate( - int player_id, std::function rng) const; + virtual std::unique_ptr + ResampleFromInfostate(int player_id, std::function rng) const; // Returns a vector of states & probabilities that are consistent with the // infostate from the view of the current player. By default, this is not @@ -761,8 +767,8 @@ class State { // current action as poker only has public actions. In a game like Battleship, // where the placement phase is hidden, this would return all possible // placements. - virtual std::vector ActionsConsistentWithInformationFrom( - Action action) const { + virtual std::vector + ActionsConsistentWithInformationFrom(Action action) const { SpielFatalError( "ActionsConsistentWithInformationFrom has not been implemented."); return {}; @@ -786,7 +792,7 @@ class State { // `DistributionSupport()[i]`. After this is called, the state will be of // Chance type. // This should only be called when when CurrentPlayer() == kMeanFieldPlayerId. - virtual void UpdateDistribution(const std::vector& distribution) { + virtual void UpdateDistribution(const std::vector &distribution) { SpielFatalError("UpdateDistribution has not been implemented"); } @@ -798,13 +804,13 @@ class State { std::unique_ptr StartingState() const; std::string StartingStateStr() const { return starting_state_str_; } - protected: +protected: // See ApplyAction. virtual void DoApplyAction(Action action_id) { SpielFatalError("DoApplyAction is not implemented."); } // See ApplyActions. - virtual void DoApplyActions(const std::vector& actions) { + virtual void DoApplyActions(const std::vector &actions) { SpielFatalError("DoApplyActions is not implemented."); } @@ -822,7 +828,7 @@ class State { std::string starting_state_str_; }; -std::ostream& operator<<(std::ostream& stream, const State& state); +std::ostream &operator<<(std::ostream &stream, const State &state); // A class that refers to a particular game instantiation, for example // Breakthrough(8x8). @@ -832,10 +838,10 @@ std::ostream& operator<<(std::ostream& stream, const State& state); // the states that created them. So, they *must* be created via // shared_ptr or via the LoadGame methods. class Game : public std::enable_shared_from_this { - public: +public: virtual ~Game() = default; - Game(const Game&) = delete; - Game& operator=(const Game&) = delete; + Game(const Game &) = delete; + Game &operator=(const Game &) = delete; // Maximum number of distinct actions in the game for any one player. This is // not the same as max number of legal actions in any state as distinct @@ -854,18 +860,18 @@ class Game : public std::enable_shared_from_this { // Return a new state from a string description. Defaults to interpreting the // string as json. - virtual std::unique_ptr NewInitialState(const std::string& str) const { + virtual std::unique_ptr NewInitialState(const std::string &str) const { return NewInitialState(nlohmann::json::parse(str)); } // Overload for string literals to resolve ambiguity with nlohmann::json. - std::unique_ptr NewInitialState(const char* str) const { + std::unique_ptr NewInitialState(const char *str) const { return NewInitialState(std::string(str)); } // Return a new state from json. - virtual std::unique_ptr NewInitialState( - const nlohmann::json& json) const { + virtual std::unique_ptr + NewInitialState(const nlohmann::json &json) const { SpielFatalError("NewInitialState from state json is not implemented."); } @@ -884,7 +890,7 @@ class Game : public std::enable_shared_from_this { // parameter values, including defaulted values. Returns empty parameters // otherwise. GameParameters GetParameters() const { - absl::MutexLock lock(mutex_defaulted_parameters_); + absl::MutexLock lock(&mutex_defaulted_parameters_); GameParameters params = game_parameters_; params.insert(defaulted_parameters_.begin(), defaulted_parameters_.end()); return params; @@ -905,7 +911,7 @@ class Game : public std::enable_shared_from_this { // Static information on the game type. This should match the information // provided when registering the game. - const GameType& GetType() const { return game_type_; } + const GameType &GetType() const { return game_type_; } // The total utility for all players, if this is a constant-sum-utility game. // Should return 0 if the game is zero-sum. @@ -967,7 +973,7 @@ class Game : public std::enable_shared_from_this { // // If this method is overridden, then it should be the inverse of // State::Serialize (i.e. that method should also be overridden). - virtual std::unique_ptr DeserializeState(const std::string& str) const; + virtual std::unique_ptr DeserializeState(const std::string &str) const; // The maximum length of any one game (in terms of number of decision nodes // visited in the game tree). For a simultaneous action game, this is the @@ -976,6 +982,11 @@ class Game : public std::enable_shared_from_this { // of chance nodes are not included in this length. virtual int MaxGameLength() const = 0; + // Returns the maximum length of the string returned by Serialize(). + // This is useful for pre-allocating memory for serialization. + // Returns 0 if not implemented. + virtual int MaxGameStringLength() const { return 0; } + // The maximum number of chance nodes occurring in any history of the game. // This is typically something like the number of times dice are rolled. // For deterministic games, this is 0, otherwise it defaults to the max game @@ -1018,7 +1029,7 @@ class Game : public std::enable_shared_from_this { std::string ToString() const; // Returns true if these games are equal, false otherwise. - virtual bool operator==(const Game& other) const { + virtual bool operator==(const Game &other) const { // GetParameters() includes default values. So comparing GetParameters // instead of game_parameters_ makes sure that game equality is independent // of the presence of explicitly passed game parameters with default values. @@ -1036,7 +1047,7 @@ class Game : public std::enable_shared_from_this { // SetRNGState is const despite the fact that it changes game's internal // state. Sampled stochastic games need to be explicit about mutability of the // RNG, i.e. have to use the mutable keyword. - virtual void SetRNGState(const std::string& rng_state) const { + virtual void SetRNGState(const std::string &rng_state) const { SpielFatalError("SetRNGState unimplemented."); } @@ -1048,9 +1059,9 @@ class Game : public std::enable_shared_from_this { // Games can include additional observation fields when requested by // `params`. // See `observer.h` for further information. - virtual std::shared_ptr MakeObserver( - absl::optional iig_obs_type, - const GameParameters& params) const; + virtual std::shared_ptr + MakeObserver(absl::optional iig_obs_type, + const GameParameters ¶ms) const; // Returns a string representation of the specified action for the player, // independent of the state. @@ -1059,26 +1070,26 @@ class Game : public std::enable_shared_from_this { } // Returns an observer that was registered, based on its name. - std::shared_ptr MakeRegisteredObserver( - absl::optional iig_obs_type, - const GameParameters& params) const; + std::shared_ptr + MakeRegisteredObserver(absl::optional iig_obs_type, + const GameParameters ¶ms) const; // Returns an observer that uses the observation or informationstate tensor // or string as defined directly on the state. Returns a nullptr if the // requested iig_obs_type is not supported. - std::shared_ptr MakeBuiltInObserver( - absl::optional iig_obs_type) const; + std::shared_ptr + MakeBuiltInObserver(absl::optional iig_obs_type) const; // Public member functions below only apply to games with mean field dynamics. // Creates a new initial state for the given population (which must be in [0, // NumPlayers())). This must be implemented for multi-population mean field // games. - virtual std::unique_ptr NewInitialStateForPopulation( - int population) const { + virtual std::unique_ptr + NewInitialStateForPopulation(int population) const { SpielFatalError("NewInitialStateForPopulation is not implemented."); } - protected: +protected: Game(GameType game_type, GameParameters game_parameters) : game_type_(game_type), game_parameters_(game_parameters) {} @@ -1087,7 +1098,7 @@ class Game : public std::enable_shared_from_this { // game_type.parameter_specification if the `default_value` is absl::nullopt // - Returns `default_value` if provided. template - T ParameterValue(const std::string& key, + T ParameterValue(const std::string &key, absl::optional default_value = absl::nullopt) const { // Return the value if found. auto iter = game_parameters_.find(key); @@ -1109,7 +1120,7 @@ class Game : public std::enable_shared_from_this { } // Return the default value, storing it. - absl::MutexLock lock(mutex_defaulted_parameters_); + absl::MutexLock lock(&mutex_defaulted_parameters_); iter = defaulted_parameters_.find(key); if (iter == defaulted_parameters_.end()) { // We haven't previously defaulted this value, so store the default we @@ -1138,8 +1149,8 @@ class Game : public std::enable_shared_from_this { // Track the parameters for which a default value has been used. This // enables us to report the actual value used for every parameter. - mutable GameParameters defaulted_parameters_ - ABSL_GUARDED_BY(mutex_defaulted_parameters_); + mutable GameParameters + defaulted_parameters_ ABSL_GUARDED_BY(mutex_defaulted_parameters_); mutable absl::Mutex mutex_defaulted_parameters_; }; @@ -1152,43 +1163,43 @@ inline std::unique_ptr State::StartingState() const { #define CONCAT_(x, y) x##y #define CONCAT(x, y) CONCAT_(x, y) -#define REGISTER_SPIEL_GAME(info, factory) \ +#define REGISTER_SPIEL_GAME(info, factory) \ GameRegisterer CONCAT(game, __COUNTER__)(info, factory); class GameRegisterer { - public: +public: using CreateFunc = - std::function(const GameParameters& params)>; + std::function(const GameParameters ¶ms)>; - GameRegisterer(const GameType& game_type, CreateFunc creator); + GameRegisterer(const GameType &game_type, CreateFunc creator); - static std::shared_ptr CreateByName(const std::string& short_name, - const GameParameters& params); + static std::shared_ptr CreateByName(const std::string &short_name, + const GameParameters ¶ms); static std::vector GamesWithKnownIssues(); static std::vector RegisteredNames(); static std::vector RegisteredConcreteNames(); static std::vector RegisteredGames(); static std::vector RegisteredConcreteGames(); - static bool IsValidName(const std::string& short_name); - static void RegisterGame(const GameType& game_type, CreateFunc creator); + static bool IsValidName(const std::string &short_name); + static void RegisterGame(const GameType &game_type, CreateFunc creator); - private: +private: // Returns a "global" map of registrations (i.e. an object that lives from // initialization to the end of the program). Note that we do not just use // a static data member, as we want the map to be initialized before first // use. - static std::map>& factories() { + static std::map> &factories() { static std::map> impl; return impl; } - static std::vector GameTypesToShortNames( - const std::vector& game_types); + static std::vector + GameTypesToShortNames(const std::vector &game_types); }; // Returns true if the game is registered, false otherwise. -bool IsGameRegistered(const std::string& short_name); +bool IsGameRegistered(const std::string &short_name); // Returns a list of registered games' short names. std::vector RegisteredGames(); @@ -1196,22 +1207,22 @@ std::vector RegisteredGames(); // Returns a list of registered game types. std::vector RegisteredGameTypes(); -std::shared_ptr DeserializeGame(const std::string& serialized); +std::shared_ptr DeserializeGame(const std::string &serialized); // Returns a new game object from the specified string, which is the short // name plus optional parameters, e.g. "go(komi=4.5,board_size=19)" -std::shared_ptr LoadGame(const std::string& game_string); +std::shared_ptr LoadGame(const std::string &game_string); // Returns a new game object with the specified parameters. -std::shared_ptr LoadGame(const std::string& short_name, - const GameParameters& params); +std::shared_ptr LoadGame(const std::string &short_name, + const GameParameters ¶ms); // Loads multiple games from a single string separated by a separator string // (with whitespace allowed). // E.g. "tic_tac_toe / leduc_poker / breakthrough(rows=6, columns=6)". -std::vector> LoadGames( - const std::string& multi_game_string, - const std::string& separator = "/"); +std::vector> +LoadGames(const std::string &multi_game_string, + const std::string &separator = "/"); // Returns a new game object with the specified parameters; reads the name // of the game from the 'name' parameter (which is not passed to the game @@ -1220,15 +1231,15 @@ std::shared_ptr LoadGame(GameParameters params); // Normalize a policy into a proper discrete distribution where the // probabilities sum to 1. -void NormalizePolicy(ActionsAndProbs* policy); +void NormalizePolicy(ActionsAndProbs *policy); // Used to sample a policy or chance outcome distribution. // Probabilities of the actions must sum to 1. // The parameter z should be a sample from a uniform distribution on the range // [0, 1). Returns the sampled action and its probability. -std::pair SampleAction(const ActionsAndProbs& outcomes, +std::pair SampleAction(const ActionsAndProbs &outcomes, double z); -std::pair SampleAction(const ActionsAndProbs& outcomes, +std::pair SampleAction(const ActionsAndProbs &outcomes, absl::BitGenRef rng); // Serialize the game and the state into one self-contained string that can @@ -1252,7 +1263,7 @@ std::pair SampleAction(const ActionsAndProbs& outcomes, // // [State] // -std::string SerializeGameAndState(const Game& game, const State& state); +std::string SerializeGameAndState(const Game &game, const State &state); // A general deserialization which reconstructs both the game and the state, // which have been saved using the default simple implementation in @@ -1266,34 +1277,32 @@ std::string SerializeGameAndState(const Game& game, const State& state); // kSampledStochastic, as there is currently no general way to set the state's // seed. std::pair, std::unique_ptr> -DeserializeGameAndState(const std::string& serialized_state); +DeserializeGameAndState(const std::string &serialized_state); // Convert GameTypes from and to strings. Used for serialization of objects // that contain them. // Note: these are not finished! They will be finished by an external // contributor. See https://github.com/deepmind/open_spiel/issues/234 for // details. -std::string GameTypeToString(const GameType& game_type); -GameType GameTypeFromString(const std::string& game_type_str); +std::string GameTypeToString(const GameType &game_type); +GameType GameTypeFromString(const std::string &game_type_str); -std::ostream& operator<<(std::ostream& os, const State::PlayerAction& action); +std::ostream &operator<<(std::ostream &os, const State::PlayerAction &action); // Utility functions used mostly for debugging. This calls State::ActionToString // for every action. -std::vector ActionsToStrings(const State& state, - const std::vector& actions); +std::vector ActionsToStrings(const State &state, + const std::vector &actions); // Calls ActionsToStrings and then calls absl::StrJoin to concatenate all the // strings together. -std::string ActionsToString(const State& state, - const std::vector& actions); +std::string ActionsToString(const State &state, + const std::vector &actions); // A utility to broadcast an error message with game and state info. // It is a wrapper around SpielFatalError and meant to facilitate debugging. -void SpielFatalErrorWithStateInfo(const std::string& error_msg, - const Game& game, - const State& state); - +void SpielFatalErrorWithStateInfo(const std::string &error_msg, + const Game &game, const State &state); // Builds the state from a history string. Checks legalities of every action // on the way. The history string is a comma-separated actions with whitespace @@ -1301,11 +1310,10 @@ void SpielFatalErrorWithStateInfo(const std::string& error_msg, // E.g. "[1, 3, 4, 5, 6]" and "57,12,72,85" are both valid. // Proceeds up to a maximum of max_steps, unless max_steps is negative, in // which case it proceeds until the end of the sequence. -std::pair, - std::unique_ptr> BuildStateFromHistoryString( - const std::string& game_string, const std::string& history, - int max_steps = -1); +std::pair, std::unique_ptr> +BuildStateFromHistoryString(const std::string &game_string, + const std::string &history, int max_steps = -1); -} // namespace open_spiel +} // namespace open_spiel -#endif // OPEN_SPIEL_SPIEL_H_ +#endif // OPEN_SPIEL_SPIEL_H_ From ec1fe9eeb2245419b1131a3564b5f3fd515ecfa0 Mon Sep 17 00:00:00 2001 From: SH20RAJ Date: Thu, 29 Jan 2026 22:17:43 +0530 Subject: [PATCH 2/2] feat: Implement Serialize for MCTSBot (#1257) - Added Serialize/Deserialize to MCTSBot - Enabled mcts_test in CMakeLists.txt (was missing) - Added unit test for serialization verifying determinism --- open_spiel/algorithms/CMakeLists.txt | 4 + open_spiel/algorithms/mcts.cc | 191 +++++++++++++++++---------- open_spiel/algorithms/mcts.h | 5 + open_spiel/algorithms/mcts_test.cc | 96 ++++++++++---- 4 files changed, 198 insertions(+), 98 deletions(-) diff --git a/open_spiel/algorithms/CMakeLists.txt b/open_spiel/algorithms/CMakeLists.txt index ff810b9266..b3a7819da5 100644 --- a/open_spiel/algorithms/CMakeLists.txt +++ b/open_spiel/algorithms/CMakeLists.txt @@ -140,6 +140,10 @@ add_executable(is_mcts_test is_mcts_test.cc $ ${OPEN_SPIEL_OBJECTS}) add_test(is_mcts_test is_mcts_test) +add_executable(mcts_test mcts_test.cc + $ ${OPEN_SPIEL_OBJECTS}) +add_test(mcts_test mcts_test) + add_executable(matrix_game_utils_test matrix_game_utils_test.cc $ ${OPEN_SPIEL_OBJECTS}) add_test(matrix_game_utils_test matrix_game_utils_test) diff --git a/open_spiel/algorithms/mcts.cc b/open_spiel/algorithms/mcts.cc index e575e97e08..4c950383c6 100644 --- a/open_spiel/algorithms/mcts.cc +++ b/open_spiel/algorithms/mcts.cc @@ -36,11 +36,9 @@ namespace algorithms { int MIN_GC_LIMIT = 5; -int MemoryUsedMb(int nodes) { - return nodes * sizeof(SearchNode) / (1 << 20); -} +int MemoryUsedMb(int nodes) { return nodes * sizeof(SearchNode) / (1 << 20); } -std::vector RandomRolloutEvaluator::Evaluate(const State& state) { +std::vector RandomRolloutEvaluator::Evaluate(const State &state) { std::vector result; for (int i = 0; i < n_rollouts_; ++i) { std::unique_ptr working_state = state.Clone(); @@ -71,7 +69,7 @@ std::vector RandomRolloutEvaluator::Evaluate(const State& state) { return result; } -ActionsAndProbs RandomRolloutEvaluator::Prior(const State& state) { +ActionsAndProbs RandomRolloutEvaluator::Prior(const State &state) { // Returns equal probability for all actions. if (state.IsChanceNode()) { return state.ChanceOutcomes(); @@ -79,7 +77,7 @@ ActionsAndProbs RandomRolloutEvaluator::Prior(const State& state) { std::vector legal_actions = state.LegalActions(); ActionsAndProbs prior; prior.reserve(legal_actions.size()); - for (const Action& action : legal_actions) { + for (const Action &action : legal_actions) { prior.emplace_back(action, 1.0 / legal_actions.size()); } return prior; @@ -92,7 +90,8 @@ double SearchNode::UCTValue(int parent_explore_count, double uct_c) const { return outcome[player]; } - if (explore_count == 0) return std::numeric_limits::infinity(); + if (explore_count == 0) + return std::numeric_limits::infinity(); // The "greedy-value" of choosing a given child is always with respect to // the current player for this node. @@ -111,7 +110,7 @@ double SearchNode::PUCTValue(int parent_explore_count, double uct_c) const { (explore_count + 1)); } -bool SearchNode::CompareFinal(const SearchNode& b) const { +bool SearchNode::CompareFinal(const SearchNode &b) const { double out = (player >= 0 && player < outcome.size() ? outcome[player] : 0); double out_b = (b.player >= 0 && b.player < b.outcome.size() ? b.outcome[b.player] : 0); @@ -124,7 +123,7 @@ bool SearchNode::CompareFinal(const SearchNode& b) const { return total_reward < b.total_reward; } -const SearchNode& SearchNode::BestChild() const { +const SearchNode &SearchNode::BestChild() const { // Returns the best action from this node, either proven or most visited. // // This ordering leads to choosing: @@ -137,31 +136,31 @@ const SearchNode& SearchNode::BestChild() const { // - Highest expected reward if explore counts are equal (unlikely). // - Longest win, if multiple are proven (unlikely due to early stopping). return *std::max_element(children.begin(), children.end(), - [](const SearchNode& a, const SearchNode& b) { + [](const SearchNode &a, const SearchNode &b) { return a.CompareFinal(b); }); } -std::string SearchNode::ChildrenStr(const State& state) const { +std::string SearchNode::ChildrenStr(const State &state) const { std::string out; if (!children.empty()) { - std::vector refs; // Sort a list of refs, not a copy. + std::vector refs; // Sort a list of refs, not a copy. refs.reserve(children.size()); - for (const SearchNode& child : children) { + for (const SearchNode &child : children) { refs.push_back(&child); } std::sort(refs.begin(), refs.end(), - [](const SearchNode* a, const SearchNode* b) { + [](const SearchNode *a, const SearchNode *b) { return b->CompareFinal(*a); }); - for (const SearchNode* child : refs) { + for (const SearchNode *child : refs) { absl::StrAppend(&out, child->ToString(state), "\n"); } } return out; } -std::string SearchNode::ToString(const State& state) const { +std::string SearchNode::ToString(const State &state) const { return absl::StrFormat( "%6s: player: %d, prior: %5.3f, value: %6.3f, sims: %5d, outcome: %s, " "%3d children", @@ -176,9 +175,8 @@ std::string SearchNode::ToString(const State& state) const { children.size()); } -Action SearchNode::SampleFromPrior(const State& state, - Evaluator* evaluator, - std::mt19937* rng) const { +Action SearchNode::SampleFromPrior(const State &state, Evaluator *evaluator, + std::mt19937 *rng) const { std::unique_ptr working_state = state.Clone(); ActionsAndProbs prior = evaluator->Prior(*working_state); Action chosen_action = SampleAction(prior, *rng).first; @@ -186,7 +184,7 @@ Action SearchNode::SampleFromPrior(const State& state, } std::vector dirichlet_noise(int count, double alpha, - std::mt19937* rng) { + std::mt19937 *rng) { std::vector noise; noise.reserve(count); @@ -196,32 +194,25 @@ std::vector dirichlet_noise(int count, double alpha, } double sum = absl::c_accumulate(noise, 0.0); - for (double& v : noise) { + for (double &v : noise) { v /= sum; } return noise; } -MCTSBot::MCTSBot(const Game& game, std::shared_ptr evaluator, +MCTSBot::MCTSBot(const Game &game, std::shared_ptr evaluator, double uct_c, int max_simulations, int64_t max_memory_mb, bool solve, int seed, bool verbose, ChildSelectionPolicy child_selection_policy, double dirichlet_alpha, double dirichlet_epsilon, bool dont_return_chance_node) - : uct_c_{uct_c}, - max_simulations_{max_simulations}, - max_nodes_((max_memory_mb << 20) / sizeof(SearchNode) + 1), - nodes_(0), - gc_limit_(MIN_GC_LIMIT), - verbose_(verbose), - solve_(solve), - max_utility_(game.MaxUtility()), - dirichlet_alpha_(dirichlet_alpha), + : uct_c_{uct_c}, max_simulations_{max_simulations}, + max_nodes_((max_memory_mb << 20) / sizeof(SearchNode) + 1), nodes_(0), + gc_limit_(MIN_GC_LIMIT), verbose_(verbose), solve_(solve), + max_utility_(game.MaxUtility()), dirichlet_alpha_(dirichlet_alpha), dirichlet_epsilon_(dirichlet_epsilon), - dont_return_chance_node_(dont_return_chance_node), - rng_(seed), - child_selection_policy_(child_selection_policy), - evaluator_(evaluator) { + dont_return_chance_node_(dont_return_chance_node), rng_(seed), + child_selection_policy_(child_selection_policy), evaluator_(evaluator) { GameType game_type = game.GetType(); if (game_type.reward_model != GameType::RewardModel::kTerminal) SpielFatalError("Game must have terminal rewards."); @@ -229,7 +220,7 @@ MCTSBot::MCTSBot(const Game& game, std::shared_ptr evaluator, SpielFatalError("Game must have sequential turns."); } -Action MCTSBot::Step(const State& state) { +Action MCTSBot::Step(const State &state) { absl::Time start = absl::Now(); std::unique_ptr root = MCTSearch(state); @@ -238,7 +229,7 @@ Action MCTSBot::Step(const State& state) { return root->SampleFromPrior(state, evaluator_.get(), &rng_); } else { // return best action - const SearchNode& best = root->BestChild(); + const SearchNode &best = root->BestChild(); if (verbose_) { double seconds = absl::ToDoubleSeconds(absl::Now() - start); @@ -264,17 +255,17 @@ Action MCTSBot::Step(const State& state) { } } -std::pair MCTSBot::StepWithPolicy(const State& state) { +std::pair MCTSBot::StepWithPolicy(const State &state) { Action action = Step(state); return {{{action, 1.}}, action}; } -std::unique_ptr MCTSBot::ApplyTreePolicy( - SearchNode* root, const State& state, - std::vector* visit_path) { +std::unique_ptr +MCTSBot::ApplyTreePolicy(SearchNode *root, const State &state, + std::vector *visit_path) { visit_path->push_back(root); std::unique_ptr working_state = state.Clone(); - SearchNode* current_node = root; + SearchNode *current_node = root; while ((!working_state->IsTerminal() && current_node->explore_count > 0) || (working_state->IsChanceNode() && dont_return_chance_node_)) { if (current_node->children.empty()) { @@ -302,18 +293,18 @@ std::unique_ptr MCTSBot::ApplyTreePolicy( Action selected_action; if (current_node->children.empty()) { // no children, sample from prior - selected_action = current_node->SampleFromPrior(state, evaluator_.get(), - &rng_); + selected_action = + current_node->SampleFromPrior(state, evaluator_.get(), &rng_); } else { // look at children - SearchNode* chosen_child = nullptr; + SearchNode *chosen_child = nullptr; if (working_state->IsChanceNode()) { // For chance nodes, rollout according to chance node's probability // distribution Action chosen_action = SampleAction(working_state->ChanceOutcomes(), rng_).first; - for (SearchNode& child : current_node->children) { + for (SearchNode &child : current_node->children) { if (child.action == chosen_action) { chosen_child = &child; break; @@ -322,15 +313,15 @@ std::unique_ptr MCTSBot::ApplyTreePolicy( } else { // Otherwise choose node with largest UCT value. double max_value = -std::numeric_limits::infinity(); - for (SearchNode& child : current_node->children) { + for (SearchNode &child : current_node->children) { double val; switch (child_selection_policy_) { - case ChildSelectionPolicy::UCT: - val = child.UCTValue(current_node->explore_count, uct_c_); - break; - case ChildSelectionPolicy::PUCT: - val = child.PUCTValue(current_node->explore_count, uct_c_); - break; + case ChildSelectionPolicy::UCT: + val = child.UCTValue(current_node->explore_count, uct_c_); + break; + case ChildSelectionPolicy::PUCT: + val = child.PUCTValue(current_node->explore_count, uct_c_); + break; } if (val > max_value) { max_value = val; @@ -349,12 +340,12 @@ std::unique_ptr MCTSBot::ApplyTreePolicy( return working_state; } -std::unique_ptr MCTSBot::MCTSearch(const State& state) { +std::unique_ptr MCTSBot::MCTSearch(const State &state) { nodes_ = 1; gc_limit_ = MIN_GC_LIMIT; - auto root = std::make_unique(kInvalidAction, - state.CurrentPlayer(), 1); - std::vector visit_path; + auto root = + std::make_unique(kInvalidAction, state.CurrentPlayer(), 1); + std::vector visit_path; std::vector returns; visit_path.reserve(64); for (int i = 0; i < max_simulations_; ++i) { @@ -377,7 +368,7 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { // Propagate values back. while (!visit_path.empty()) { int decision_node_idx = visit_path.size() - 1; - SearchNode* node = visit_path[decision_node_idx]; + SearchNode *node = visit_path[decision_node_idx]; // If it's a chance node, find the parent player id. while (visit_path[decision_node_idx]->player == kChancePlayerId) { @@ -395,10 +386,10 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { // Only back up chance nodes if all have the same outcome. // An alternative would be to back up the weighted average of // outcomes if all children are solved, but that is less clear. - const std::vector& outcome = node->children[0].outcome; + const std::vector &outcome = node->children[0].outcome; if (!outcome.empty() && std::all_of(node->children.begin() + 1, node->children.end(), - [&outcome](const SearchNode& c) { + [&outcome](const SearchNode &c) { return c.outcome == outcome; })) { node->outcome = outcome; @@ -408,9 +399,9 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { } else { // If any have max utility (won?), or all children are solved, // choose the one best for the player choosing. - const SearchNode* best = nullptr; + const SearchNode *best = nullptr; bool all_solved = true; - for (const SearchNode& child : node->children) { + for (const SearchNode &child : node->children) { if (child.outcome.empty()) { all_solved = false; } else if (best == nullptr || @@ -428,7 +419,7 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { } } - if (!root->outcome.empty() || // Full game tree is solved. + if (!root->outcome.empty() || // Full game tree is solved. root->children.size() == 1) { break; } @@ -450,9 +441,8 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { gc_limit_ *= (nodes_ > max_nodes_ / 2 ? 1.25 : 0.9); gc_limit_ = std::max(MIN_GC_LIMIT, gc_limit_); if (verbose_) { - std::cerr << absl::StrFormat( - "%d mb in %d nodes remaining\n", - MemoryUsedMb(nodes_), nodes_); + std::cerr << absl::StrFormat("%d mb in %d nodes remaining\n", + MemoryUsedMb(nodes_), nodes_); } } } @@ -460,20 +450,79 @@ std::unique_ptr MCTSBot::MCTSearch(const State& state) { return root; } -void MCTSBot::GarbageCollect(SearchNode* node) { +void MCTSBot::GarbageCollect(SearchNode *node) { if (node->children.empty()) { return; } bool clear_children = node->explore_count < gc_limit_; - for (SearchNode& child : node->children) { + for (SearchNode &child : node->children) { GarbageCollect(&child); } if (clear_children) { nodes_ -= node->children.capacity(); node->children.clear(); - node->children.shrink_to_fit(); // release the memory + node->children.shrink_to_fit(); // release the memory } } -} // namespace algorithms -} // namespace open_spiel +std::string MCTSBot::Serialize() const { + std::ostringstream os; + os << uct_c_ << "\n"; + os << max_simulations_ << "\n"; + os << max_nodes_ << "\n"; + os << solve_ << "\n"; + os << max_utility_ << "\n"; + os << dirichlet_alpha_ << "\n"; + os << dirichlet_epsilon_ << "\n"; + os << dont_return_chance_node_ << "\n"; + os << verbose_ << "\n"; + os << static_cast(child_selection_policy_) << "\n"; + os << rng_; // Serialization of mt19937 + return os.str(); +} + +std::unique_ptr +MCTSBot::Deserialize(const std::string &str, std::shared_ptr game, + std::shared_ptr evaluator) { + std::istringstream is(str); + double uct_c; + int max_simulations; + int max_nodes; + bool solve; + double max_utility; + double dirichlet_alpha; + double dirichlet_epsilon; + bool dont_return_chance_node; + bool verbose; + int child_selection_policy_int; + + is >> uct_c; + is >> max_simulations; + is >> max_nodes; + is >> solve; + is >> max_utility; + is >> dirichlet_alpha; + is >> dirichlet_epsilon; + is >> dont_return_chance_node; + is >> verbose; + is >> child_selection_policy_int; + + int64_t max_memory_mb = + (static_cast(max_nodes) * sizeof(SearchNode)) / (1 << 20); + if (max_memory_mb < 1) + max_memory_mb = 1; + + auto bot = std::make_unique( + *game, evaluator, uct_c, max_simulations, max_memory_mb, solve, + /*seed=*/0, verbose, + static_cast(child_selection_policy_int), + dirichlet_alpha, dirichlet_epsilon, dont_return_chance_node); + + // Restore RNG + is >> bot->rng_; + + return bot; +} + +} // namespace algorithms +} // namespace open_spiel diff --git a/open_spiel/algorithms/mcts.h b/open_spiel/algorithms/mcts.h index a05a78d4da..3418d20cd6 100644 --- a/open_spiel/algorithms/mcts.h +++ b/open_spiel/algorithms/mcts.h @@ -183,6 +183,11 @@ class MCTSBot : public Bot { // Run MCTS on a given state, and return the resulting search tree. std::unique_ptr MCTSearch(const State& state); + std::string Serialize() const; + static std::unique_ptr Deserialize( + const std::string& str, std::shared_ptr game, + std::shared_ptr evaluator); + private: // Applies the UCT policy to play the game until reaching a leaf node. // diff --git a/open_spiel/algorithms/mcts_test.cc b/open_spiel/algorithms/mcts_test.cc index 31b864e5bd..ea03ff0528 100644 --- a/open_spiel/algorithms/mcts_test.cc +++ b/open_spiel/algorithms/mcts_test.cc @@ -17,8 +17,8 @@ #include #include -#include "open_spiel/abseil-cpp/absl/strings/string_view.h" #include "open_spiel/abseil-cpp/absl/strings/str_split.h" +#include "open_spiel/abseil-cpp/absl/strings/string_view.h" #include "open_spiel/algorithms/evaluate_bots.h" #include "open_spiel/spiel.h" #include "open_spiel/spiel_bots.h" @@ -32,7 +32,7 @@ namespace { constexpr double UCT_C = 2; -std::unique_ptr InitBot(const open_spiel::Game& game, +std::unique_ptr InitBot(const open_spiel::Game &game, int max_simulations, std::shared_ptr evaluator) { return std::make_unique( @@ -95,7 +95,7 @@ void MCTSTest_CanPlayThreePlayerStochasticGames() { SPIEL_CHECK_FLOAT_EQ(results[0] + results[1] + results[2], 0); } -open_spiel::Action GetAction(const open_spiel::State& state, +open_spiel::Action GetAction(const open_spiel::State &state, const absl::string_view action_str) { for (open_spiel::Action action : state.LegalActions()) { if (action_str == state.ActionToString(state.CurrentPlayer(), action)) @@ -108,16 +108,16 @@ std::pair, std::unique_ptr> SearchTicTacToeState(const absl::string_view initial_actions) { auto game = LoadGame("tic_tac_toe"); std::unique_ptr state = game->NewInitialState(); - for (const auto& action_str : absl::StrSplit(initial_actions, ' ')) { + for (const auto &action_str : absl::StrSplit(initial_actions, ' ')) { state->ApplyAction(GetAction(*state, action_str)); } auto evaluator = std::make_shared(20, 42); algorithms::MCTSBot bot(*game, evaluator, UCT_C, - /*max_simulations=*/ 10000, - /*max_memory_mb=*/ 10, - /*solve=*/ true, - /*seed=*/ 42, - /*verbose=*/ false); + /*max_simulations=*/10000, + /*max_memory_mb=*/10, + /*solve=*/true, + /*seed=*/42, + /*verbose=*/false); return {bot.MCTSearch(*state), std::move(state)}; } @@ -125,13 +125,13 @@ void MCTSTest_SolveDraw() { auto [root, state] = SearchTicTacToeState("x(1,1) o(0,0) x(2,2)"); SPIEL_CHECK_EQ(state->ToString(), "o..\n.x.\n..x"); SPIEL_CHECK_EQ(root->outcome[root->player], 0); - for (const algorithms::SearchNode& c : root->children) - SPIEL_CHECK_LE(c.outcome[c.player], 0); // No winning moves. - const algorithms::SearchNode& best = root->BestChild(); + for (const algorithms::SearchNode &c : root->children) + SPIEL_CHECK_LE(c.outcome[c.player], 0); // No winning moves. + const algorithms::SearchNode &best = root->BestChild(); SPIEL_CHECK_EQ(best.outcome[best.player], 0); std::string action_str = state->ActionToString(best.player, best.action); - if (action_str != "o(2,0)" && action_str != "o(0,2)") // All others lose. - SPIEL_CHECK_EQ(action_str, "o(2,0)"); // "o(0,2)" is also valid. + if (action_str != "o(2,0)" && action_str != "o(0,2)") // All others lose. + SPIEL_CHECK_EQ(action_str, "o(2,0)"); // "o(0,2)" is also valid. } void MCTSTest_SolveLoss() { @@ -139,15 +139,15 @@ void MCTSTest_SolveLoss() { SearchTicTacToeState("x(1,1) o(0,0) x(2,2) o(0,1) x(0,2)"); SPIEL_CHECK_EQ(state->ToString(), "oox\n.x.\n..x"); SPIEL_CHECK_EQ(root->outcome[root->player], -1); - for (const algorithms::SearchNode& c : root->children) - SPIEL_CHECK_EQ(c.outcome[c.player], -1); // All losses. + for (const algorithms::SearchNode &c : root->children) + SPIEL_CHECK_EQ(c.outcome[c.player], -1); // All losses. } void MCTSTest_SolveWin() { auto [root, state] = SearchTicTacToeState("x(0,1) o(2,2)"); SPIEL_CHECK_EQ(state->ToString(), ".x.\n...\n..o"); SPIEL_CHECK_EQ(root->outcome[root->player], 1); - const algorithms::SearchNode& best = root->BestChild(); + const algorithms::SearchNode &best = root->BestChild(); SPIEL_CHECK_EQ(best.outcome[best.player], 1); SPIEL_CHECK_EQ(state->ActionToString(best.player, best.action), "x(0,2)"); } @@ -157,20 +157,61 @@ void MCTSTest_GarbageCollect() { std::unique_ptr state = game->NewInitialState(); auto evaluator = std::make_shared(1, 42); algorithms::MCTSBot bot(*game, evaluator, UCT_C, - /*max_simulations=*/ 1000000, - /*max_memory_mb=*/ 1, - /*solve=*/ true, - /*seed=*/ 42, - /*verbose=*/ true); // Verify the log output. + /*max_simulations=*/1000000, + /*max_memory_mb=*/1, + /*solve=*/true, + /*seed=*/42, + /*verbose=*/true); // Verify the log output. std::unique_ptr root = bot.MCTSearch(*state); - SPIEL_CHECK_TRUE(root->outcome.size() == 2 || - root->explore_count == 1000000); + SPIEL_CHECK_TRUE(root->outcome.size() == 2 || root->explore_count == 1000000); +} + +class ConstantEvaluator : public Evaluator { +public: + std::vector Evaluate(const State &state) override { + return std::vector(state.NumPlayers(), 0.5); + } + ActionsAndProbs Prior(const State &state) override { + auto actions = state.LegalActions(); + ActionsAndProbs probs; + if (actions.empty()) + return probs; + for (auto a : actions) + probs.push_back({a, 1.0 / actions.size()}); + return probs; + } +}; + +void MCTSTest_SerializeDeserialize() { + auto game = LoadGame("tic_tac_toe"); + auto evaluator = std::make_shared(); + + // Use non-default params to verify parameter persistence + double uct_c = 3.0; + int max_simulations = 50; + algorithms::MCTSBot bot(*game, evaluator, uct_c, max_simulations, + /*max_memory_mb=*/10, /*solve=*/true, /*seed=*/42, + /*verbose=*/false); + + auto state = game->NewInitialState(); + bot.Step(*state); // Advance RNG + + std::string serialized = bot.Serialize(); + auto bot2 = algorithms::MCTSBot::Deserialize(serialized, game, evaluator); + + auto state2 = game->NewInitialState(); + for (int i = 0; i < 20; ++i) { + Action a1 = bot.Step(*state2); + Action a2 = bot2->Step(*state2); + SPIEL_CHECK_EQ(a1, a2); + } } -} // namespace -} // namespace open_spiel +} // namespace + +} // namespace open_spiel -int main(int argc, char** argv) { +int main(int argc, char **argv) { open_spiel::MCTSTest_CanPlayTicTacToe(); open_spiel::MCTSTest_CanPlayTicTacToe_LowSimulations(); open_spiel::MCTSTest_CanPlayBothSides(); @@ -180,4 +221,5 @@ int main(int argc, char** argv) { open_spiel::MCTSTest_SolveLoss(); open_spiel::MCTSTest_SolveWin(); open_spiel::MCTSTest_GarbageCollect(); + open_spiel::MCTSTest_SerializeDeserialize(); }