diff --git a/open_spiel/CMakeLists.txt b/open_spiel/CMakeLists.txt index 880a9365ae..b0f11435d3 100644 --- a/open_spiel/CMakeLists.txt +++ b/open_spiel/CMakeLists.txt @@ -50,7 +50,7 @@ if(${BUILD_TYPE} STREQUAL "Testing") # A build used for running tests: keep all runtime checks (assert, # SPIEL_CHECK_*, SPIEL_DCHECK_*), but turn on some speed optimizations, # otherwise tests run for too long. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 ") endif() if(${BUILD_TYPE} STREQUAL "Release") @@ -295,6 +295,7 @@ if (OPEN_SPIEL_BUILD_WITH_TENSORFLOW_CC) find_package(TensorflowCC REQUIRED) endif() + # We have the parent of this directory in the include path, so that we can # include for example "open_spiel/spiel.h" (assuming this directory is named # open_spiel). diff --git a/open_spiel/examples/CMakeLists.txt b/open_spiel/examples/CMakeLists.txt index 91934c09d3..96f8954033 100644 --- a/open_spiel/examples/CMakeLists.txt +++ b/open_spiel/examples/CMakeLists.txt @@ -17,6 +17,8 @@ add_executable(fsicfr_liars_dice fsicfr_liars_dice.cc ${OPEN_SPIEL_OBJECTS}) add_executable(gtp gtp.cc ${OPEN_SPIEL_OBJECTS}) +add_executable(is_mcts_gwhist is_mcts_gwhist.cc ${OPEN_SPIEL_OBJECTS}) + add_executable(matrix_example matrix_example.cc ${OPEN_SPIEL_OBJECTS}) add_test(matrix_example_test matrix_example) diff --git a/open_spiel/examples/is_mcts_gwhist.cc b/open_spiel/examples/is_mcts_gwhist.cc new file mode 100644 index 0000000000..4abf43473b --- /dev/null +++ b/open_spiel/examples/is_mcts_gwhist.cc @@ -0,0 +1,89 @@ +// Copyright 2021 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. + +#include "open_spiel/algorithms/is_mcts.h" + +#include + +#include "open_spiel/abseil-cpp/absl/random/distributions.h" +#include "open_spiel/algorithms/mcts.h" +#include "open_spiel/spiel.h" +#include "open_spiel/spiel_bots.h" +#include "open_spiel/spiel_utils.h" + +namespace open_spiel { +namespace { + +constexpr const int kSeed = 9492110;//93879211; + +void PlayGWhist(int human_player, std::mt19937* rng,int num_rollouts) { + std::shared_ptr game = LoadGame("german_whist_foregame"); + std::random_device rd; + int eval_seed = rd(); + int bot_seed = rd(); + auto evaluator = std::make_shared(1, eval_seed); + auto bot = std::make_unique( + bot_seed, evaluator, 0.7*13, num_rollouts, algorithms::kUnlimitedNumWorldSamples, + algorithms::ISMCTSFinalPolicyType::kMaxVisitCount,true, false); + std::unique_ptr state = game->NewInitialState(); + while (!state->IsTerminal()) { + + Action chosen_action = kInvalidAction; + if (state->IsChanceNode()) { + chosen_action = + SampleAction(state->ChanceOutcomes(), absl::Uniform(*rng, 0.0, 1.0)) + .first; + } else if(state->CurrentPlayer()!=human_player) { + chosen_action = bot->Step(*state); + } + else{ + std::cout<InformationStateString(human_player)<LegalActions(); + for(int i =0;iActionToString(legal_actions[i])<<","; + } + std::cout<>input; + chosen_action = state->StringToAction(input); + std::cout<ApplyAction(chosen_action); + } + + std::cout << "Terminal state:" << std::endl; + std::cout << state->ToString() << std::endl; + std::cout << "Returns: " << absl::StrJoin(state->Returns(), " ") << std::endl; +} + + +} // namespace +} // namespace open_spiel + + + +int main(int argc, char** argv) { + std::random_device rd; + std::mt19937 rng(rd()); + int human_player; + int num_rollouts; + std::cout<<"human_player:"; + std::cin>>human_player; + std::cout<<"\n"; + std::cout<<"num_rollouts:"; + std::cin>>num_rollouts; + std::cout<<"\n"; + open_spiel::PlayGWhist(human_player,&rng,num_rollouts); +} diff --git a/open_spiel/games/CMakeLists.txt b/open_spiel/games/CMakeLists.txt index 6af3133c2c..a6533cf52e 100644 --- a/open_spiel/games/CMakeLists.txt +++ b/open_spiel/games/CMakeLists.txt @@ -74,6 +74,8 @@ set(GAME_SOURCES euchre/euchre.h first_sealed_auction/first_sealed_auction.cc first_sealed_auction/first_sealed_auction.h + german_whist_foregame/german_whist_foregame.cc + german_whist_foregame/german_whist_foregame.h gin_rummy/gin_rummy.cc gin_rummy/gin_rummy.h gin_rummy/gin_rummy_utils.cc @@ -192,6 +194,7 @@ if (${OPEN_SPIEL_BUILD_WITH_ACPC}) set(GAME_SOURCES ${GAME_SOURCES} universal_poker/universal_poker.cc universal_poker/universal_poker.h) endif() + add_library (games OBJECT ${GAME_SOURCES}) target_include_directories (games PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) @@ -428,6 +431,11 @@ add_executable(garnet_test mfg/garnet_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(garnet_test garnet_test) +add_executable(german_whist_foregame_test german_whist_foregame/german_whist_foregame_test.cc ${OPEN_SPIEL_OBJECTS} + $) +add_test(german_whist_foregame_test german_whist_foregame_test) +add_executable(german_whist_endgame german_whist_foregame/german_whist_endgame.cc ${OPEN_SPIEL_OBJECTS}) + add_executable(gin_rummy_test gin_rummy/gin_rummy_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(gin_rummy_test gin_rummy_test) diff --git a/open_spiel/games/german_whist_foregame/german_whist_endgame.cc b/open_spiel/games/german_whist_foregame/german_whist_endgame.cc new file mode 100644 index 0000000000..fb6ff3d598 --- /dev/null +++ b/open_spiel/games/german_whist_foregame/german_whist_endgame.cc @@ -0,0 +1,733 @@ +// Source Code for an Executable Generating an Endgame Tablebase for German +// Whist + +#include + +#include "open_spiel/games/german_whist_foregame/german_whist_foregame.h" +#include "open_spiel/utils/file.h" +#include "open_spiel/utils/thread.h" + +// #define DEBUG +namespace open_spiel { +namespace german_whist_foregame { + +struct Pair { + char index; + char value; + Pair(char index_, char value_) { + index = index_; + value = value_; + } + bool operator<(const Pair& pair) const { return value < pair.value; } +}; +struct ActionStruct { + uint32_t index; + unsigned char suit; + bool player; + ActionStruct(uint32_t index_, unsigned char suit_, bool player_) { + index = index_; + suit = suit_; + player = player_; + } +}; +struct ActionValue { + ActionStruct action; + int value; + bool operator<(const ActionValue& aval) const { return value < aval.value; } +}; + +class Node { + private: + uint32_t cards_; + std::array suit_masks_; + char total_tricks_; + char trump_; + char score_; + char moves_; + bool player_; + std::vector history_; + uint64_t key_; + + public: + Node(uint32_t cards, std::array suit_masks, char trump, + bool player) { + cards_ = cards; + suit_masks_ = suit_masks; + total_tricks_ = popcnt_u32(cards); + trump_ = trump; + moves_ = 0; + player_ = player; + score_ = 0; + history_ = {}; + }; + bool Player() { return player_; }; + char Score() { return score_; }; + char Moves() { return moves_; }; + bool IsTerminal() { return (moves_ == 2 * total_tricks_); } + char RemainingTricks() { return (char)(total_tricks_ - (moves_ >> 1)); } + char TotalTricks() { return total_tricks_; } + uint32_t Cards() { return cards_; } + std::array SuitMasks() { return suit_masks_; } + uint64_t GetNodeKey() { return key_; } + bool Trick(ActionStruct lead, ActionStruct follow) { + // true if leader won// + return (lead.suit != follow.suit && lead.suit == trump_) || + (lead.suit == follow.suit && lead.index <= follow.index); + } + + void RemoveCard(ActionStruct action) { + // Removes card from cards_// + uint32_t mask_b = ~0; + mask_b = bzhi_u32(mask_b, action.index); + uint32_t mask_a = ~mask_b; + mask_a = blsr_u32(mask_a); + uint32_t copy_a = cards_ & mask_a; + uint32_t copy_b = cards_ & mask_b; + copy_a = copy_a >> 1; + cards_ = copy_a | copy_b; + // decrements appropriate suits// + suit_masks_[action.suit] = blsr_u32(suit_masks_[action.suit]) >> 1; + char suit = action.suit; + suit++; + while (suit < kNumSuits) { + suit_masks_[suit] = suit_masks_[suit] >> 1; + suit++; + } + } + void InsertCard(ActionStruct action) { + // inserts card into cards_// + uint32_t mask_b = ~0; + mask_b = bzhi_u32(mask_b, action.index); + uint32_t mask_a = ~mask_b; + uint32_t copy_b = cards_ & mask_b; + uint32_t copy_a = cards_ & mask_a; + copy_a = copy_a << 1; + uint32_t card = action.player << action.index; + cards_ = card | copy_a | copy_b; + // increments appropriate suits// + uint32_t new_suit = + (suit_masks_[action.suit] & mask_b) | (1 << action.index); + suit_masks_[action.suit] = + ((suit_masks_[action.suit] & mask_a) << 1) | new_suit; + char suit = action.suit; + suit++; + while (suit < kNumSuits) { + suit_masks_[suit] = suit_masks_[suit] << 1; + suit++; + } + } + void UpdateNodeKey() { + // recasts the cards and suitlengths into quasi-canonical form// + // least sig part of 32bit card is trump, then suits in ascending length// + + // note this canonical form does not take advantage of all isomorphisms// + // suppose a game is transformed as follows: all card bits flipped and the + // player bit flipped, ie player 1 has the lead and has player 0s cards from + // the original game// this implies player 1 achieves the minimax value of + // the original game ie the value is remaining tricks - value of the + // original game for this transformed game// also does not take advantage of + // single suit isomorphism. Namely all single suit games with the same card + // distribution are isomorphic. Currently this considers all trump, all no + // trump games as distinct// + uint64_t suit_sig = 0; + char trump_length = popcnt_u32(suit_masks_[trump_]); + if (trump_length > kNumRanks) { + throw; + } + std::vector non_trump_lengths; + for (char i = 0; i < kNumSuits; ++i) { + if (i != trump_) { + char length = popcnt_u32(suit_masks_[i]); + uint32_t sig = suit_masks_[i] & cards_; + if (suit_masks_[i] != 0) { + sig = (sig >> (tzcnt_u32(suit_masks_[i]))); + } + if (length > kNumRanks) { + throw 1; + } + non_trump_lengths.push_back(Triple{i, length, sig}); + } + } + // sorting takes advantage of two isomorphisms namely nontrump suits of + // nonequal length can be exchanged and the value of the game does not + // change// and this more complicated suppose two games with two or more + // (non_trump)suits of equal length, permuting those suits should not change + // the value of solved game ie it is an isomorphism// + std::sort(non_trump_lengths.begin(), non_trump_lengths.end()); + suit_sig = suit_sig | trump_length; + for (size_t i = 0; i < non_trump_lengths.size(); ++i) { + suit_sig = + suit_sig | ((uint64_t)non_trump_lengths[i].length << (4 * (i + 1))); + } + suit_sig = suit_sig << 32; + std::array suit_cards; + suit_cards[0] = cards_ & suit_masks_[trump_]; + if (suit_masks_[trump_] != 0) { + suit_cards[0] = suit_cards[0] >> tzcnt_u32(suit_masks_[trump_]); + } + uint32_t sum = popcnt_u32(suit_masks_[trump_]); + uint32_t cards = 0 | suit_cards[0]; + for (size_t i = 0; i < non_trump_lengths.size(); ++i) { + suit_cards[i] = cards_ & suit_masks_[non_trump_lengths[i].index]; + uint32_t val = 0; + if (suit_masks_[non_trump_lengths[i].index] != 0) { + val = tzcnt_u32(suit_masks_[non_trump_lengths[i].index]); + } + suit_cards[i] = suit_cards[i] >> val; + suit_cards[i] = suit_cards[i] << sum; + sum += popcnt_u32(suit_masks_[non_trump_lengths[i].index]); + cards = cards | suit_cards[i]; + } + // cards = cards | (player_ << 31); + key_ = suit_sig | (uint64_t)cards; +#ifdef DEBUG_KEY + std::cout << "CARDS_ " << cards_ << std::endl; + std::cout << "CARDS " << cards << std::endl; + std::cout << "SUIT MASKS " << std::endl; + for (int i = 0; i < kNumSuits; ++i) { + std::cout << suit_masks_[i] << std::endl; + } + std::cout << "SUIT_SIG " << suit_sig << std::endl; + std::cout << "KEY " << key_ << std::endl; +#endif + } + uint64_t AltKey() { + uint32_t mask = bzhi_u32(~0, 2 * RemainingTricks()); + return key_ ^ (uint64_t)mask; + } + // Move Ordering Heuristics// + // These could Definitely be improved, very hacky// + int LeadOrdering(ActionStruct action) { + char suit = action.suit; + uint32_t copy_cards = cards_; + if (player_ == 0) { + copy_cards = ~copy_cards; + } + uint32_t suit_cards = copy_cards & suit_masks_[suit]; + uint32_t mask = suit_cards & ~(suit_cards >> 1); + // represents out of the stategically inequivalent cards in a suit that a + // player holds, what rank is it, rank 0 is highest rank etc// + int suit_rank = popcnt_u32(bzhi_u32(mask, action.index)); + ApplyAction(action); + std::vector moves = LegalActions(); + UndoAction(action); + int sum = 0; + for (size_t i = 0; i < moves.size(); ++i) { + sum += Trick(action, moves[i]); + } + if (sum == moves.size()) { + return action.suit == trump_ + ? 0 - suit_rank + : -1 * kNumRanks - + suit_rank; // intriguing this seems to produce small + // perfomance increase// + } + if (sum == 0) { + return 2 * kNumRanks - suit_rank; + } else { + return 1 * kNumRanks - suit_rank; + } + } + int FollowOrdering(ActionStruct action) { + ActionStruct lead = history_.back(); + // follow ordering for fast cut offs// + // win as cheaply as possible, followed by lose as cheaply as possible + char suit = action.suit; + uint32_t copy_cards = cards_; + if (player_ == 0) { + copy_cards = ~copy_cards; + } + uint32_t suit_cards = copy_cards & suit_masks_[suit]; + uint32_t mask = suit_cards & ~(suit_cards >> 1); + // represents out of the stategically inequivalent cards in a suit that a + // player holds, what rank is it, rank 0 is highest rank etc// + int suit_rank = popcnt_u32(bzhi_u32(mask, action.index)); + if (!Trick(lead, action)) { + return -kNumRanks - suit_rank; + } else { + return -suit_rank; + } + } + + std::vector LegalActions() { + // Features// + // Move fusion// + std::vector out; + out.reserve(kNumRanks); + uint32_t copy_cards = cards_; + std::array player_suit_masks; + if (player_ == 0) { + copy_cards = ~copy_cards; + } + for (size_t i = 0; i < kNumSuits; ++i) { + uint32_t suit_cards = copy_cards & suit_masks_[i]; + player_suit_masks[i] = suit_cards & ~(suit_cards >> 1); +#ifdef DEBUG + std::cout << "Cards " << cards_ << std::endl; + std::cout << "Suit Mask " << i << " " << suit_masks_[i] << std::endl; + std::cout << "Player " << player_ << " suit mask " << (int)i << " " + << player_suit_masks[i] << std::endl; +#endif + } + for (char i = 0; i < kNumSuits; ++i) { + uint32_t suit_mask = player_suit_masks[i]; + bool lead = (moves_ % 2 == 0); + bool follow = (moves_ % 2 == 1); + bool correct_suit = 0; + bool void_in_suit = 0; + if (follow == true) { + correct_suit = (history_.back().suit == i); + void_in_suit = (player_suit_masks[history_.back().suit] == 0); + } + if ((lead || (follow && (correct_suit || void_in_suit)))) { + while (suit_mask != 0) { + uint32_t best = tzcnt_u32(suit_mask); + out.push_back(ActionStruct(best, i, player_)); + suit_mask = blsr_u32(suit_mask); + } + } + } +#ifdef DEBUG + std::cout << "Player " << player_ << " MoveGen " << std::endl; + for (size_t i = 0; i < out.size(); ++i) { + std::cout << out[i].index << " " << (int)out[i].suit << std::endl; + } +#endif + return out; + } + void ApplyAction(ActionStruct action) { +#ifdef DEBUG + std::cout << "Player " << player_ << " ApplyAction " << action.index << " " + << (int)action.suit << std::endl; +#endif + if (moves_ % 2 == 1) { + ActionStruct lead = history_.back(); + bool winner = !((Trick(lead, action)) ^ lead.player); +#ifdef DEBUG + std::cout << "Player " << winner << " won this trick" << std::endl; +#endif + score_ += (winner == 0); + player_ = (winner); + } else { + player_ = !player_; + } +#ifdef DEBUG + assert((suit_masks_[0] & suit_masks_[1]) == 0); + assert((suit_masks_[0] & suit_masks_[2]) == 0); + assert((suit_masks_[0] & suit_masks_[3]) == 0); + assert((suit_masks_[1] & suit_masks_[2]) == 0); + assert((suit_masks_[1] & suit_masks_[3]) == 0); + assert((suit_masks_[2] & suit_masks_[3]) == 0); +#endif + RemoveCard(action); + moves_++; + history_.push_back(action); + } + void UndoAction(ActionStruct action) { + if (moves_ % 2 == 0) { + ActionStruct lead = history_[history_.size() - 2]; + ActionStruct follow = history_[history_.size() - 1]; + bool winner = !(Trick(lead, follow) ^ lead.player); + score_ -= (winner == 0); + } + InsertCard(action); + moves_--; + player_ = history_.back().player; + history_.pop_back(); +#ifdef DEBUG + std::cout << "Player " << player_ << " UndoAction " << action.index << " " + << (int)action.suit << std::endl; +#endif + } +}; + +// solvers below +int AlphaBeta(Node* node, int alpha, int beta) { + // fail soft ab search// + // uses move ordering to speed up search// + if (node->IsTerminal()) { + return node->Score(); + } + // move ordering code// + std::vector actions = node->LegalActions(); + std::vector temp; + temp.reserve(kNumRanks); + for (int i = 0; i < actions.size(); ++i) { + if (node->Moves() % 2 == 0) { + temp.push_back({actions[i], node->LeadOrdering(actions[i])}); + } else { + temp.push_back({actions[i], node->FollowOrdering(actions[i])}); + } + } + std::sort(temp.begin(), temp.end()); + for (int i = 0; i < temp.size(); ++i) { + actions[i] = temp[i].action; + } + // alpha beta search// + if (node->Player() == 0) { + int val = 0; + for (int i = 0; i < actions.size(); ++i) { + node->ApplyAction(actions[i]); + val = std::max(val, AlphaBeta(node, alpha, beta)); + node->UndoAction(actions[i]); + alpha = std::max(val, alpha); + if (val >= beta) { + break; + } + } + return val; + } else if (node->Player() == 1) { + int val = node->TotalTricks(); + for (int i = 0; i < actions.size(); ++i) { + node->ApplyAction(actions[i]); + val = std::min(val, AlphaBeta(node, alpha, beta)); + node->UndoAction(actions[i]); + beta = std::min(val, beta); + if (val <= alpha) { + break; + } + } + return val; + } + return -1; +}; + +// Helper Functions// + +// Credit to computationalcombinatorics.wordpress.com +// hideous code for generating the next colexicographical combination// +bool NextColex(std::vector& v, int k) { + int num = 0; + for (int i = 0; i < v.size(); ++i) { + if (i == v.size() - 1) { + v[i] = v[i] + 1; + if (v[i] > k - v.size() + i) { + return false; + } + num = i; + break; + } else if (v[i + 1] - v[i] > 1 && v[i + 1] != i) { + v[i] = v[i] + 1; + if (v[i] > k - v.size() + i) { + return false; + } + num = i; + break; + } + } + for (int i = 0; i < num; ++i) { + v[i] = i; + } + return true; +} + +char IncrementalAlphaBetaMemoryIso( + Node* node, char alpha, char beta, int depth, const vectorNa* TTable, + const std::unordered_map* SuitRanks, + const std::vector>& bin_coeffs) { + // fail soft ab search + char val = 0; + uint64_t key = 0; + bool player = node->Player(); + if (node->IsTerminal()) { + return node->Score(); + } + if (node->Moves() % 2 == 0 && depth == 0) { + node->UpdateNodeKey(); + key = (player) ? node->AltKey() : node->GetNodeKey(); + uint32_t cards = key & bzhi_u64(~0, 32); + uint32_t colex = HalfColexer(cards, &bin_coeffs); + uint32_t suits = (key & (~0 ^ bzhi_u64(~0, 32))) >> 32; + uint32_t suit_rank = SuitRanks->at(suits); + char value = (player) + ? node->RemainingTricks() - TTable->Get(colex, suit_rank) + : TTable->Get(colex, suit_rank); + return value + node->Score(); + } else if (node->Player() == 0) { + val = 0; + std::vector actions = node->LegalActions(); + for (int i = 0; i < actions.size(); ++i) { + node->ApplyAction(actions[i]); + val = std::max( + val, IncrementalAlphaBetaMemoryIso(node, alpha, beta, depth - 1, + TTable, SuitRanks, bin_coeffs)); + node->UndoAction(actions[i]); + alpha = std::max(val, alpha); + if (val >= beta) { + break; + } + } + } else if (node->Player() == 1) { + val = node->TotalTricks(); + std::vector actions = node->LegalActions(); + for (int i = 0; i < actions.size(); ++i) { + node->ApplyAction(actions[i]); + val = std::min( + val, IncrementalAlphaBetaMemoryIso(node, alpha, beta, depth - 1, + TTable, SuitRanks, bin_coeffs)); + node->UndoAction(actions[i]); + beta = std::min(val, beta); + if (val <= alpha) { + break; + } + } + } + return val; +}; + +std::vector GWhistGenerator(int num, unsigned int seed) { + // generates pseudorandom endgames// + std::vector out; + out.reserve(num); + std::mt19937 g(seed); + std::array nums; + for (int i = 0; i < 2 * kNumRanks; ++i) { + nums[i] = i; + } + for (int i = 0; i < num; ++i) { + std::shuffle(nums.begin(), nums.end(), g); + uint32_t cards = 0; + std::array suits; + for (int j = 0; j < kNumRanks; ++j) { + cards = cards | (1 << nums[j]); + } + int sum = 0; + std::vector suit_lengths = {0, 0, 0, 0}; + for (int j = 0; j < kNumSuits - 1; ++j) { + int max = std::min(kNumRanks, 2 * kNumRanks - sum); + int min = std::max(0, (j - 1) * kNumRanks - sum); + std::uniform_int_distribution<> distrib(min, max); + suit_lengths[j] = distrib(g); + sum += suit_lengths[j]; + } + suit_lengths[kNumSuits - 1] = 2 * kNumRanks - sum; + sum = 0; + for (int j = 0; j < kNumSuits; ++j) { + sum += suit_lengths[j]; + if (suit_lengths[j] > kNumRanks) { + throw; + } + } + if (sum != 2 * kNumRanks) { + for (int j = 0; j < suit_lengths.size(); ++j) { + std::cout << suit_lengths[j] << " " << std::endl; + } + throw; + } + int cum_sum = 0; + for (int j = 0; j < kNumSuits; ++j) { + if (j == 0) { + suits[j] = bzhi_u32(~0, suit_lengths[j]); + } else { + suits[j] = + (bzhi_u32(~0, suit_lengths[j] + cum_sum)) ^ bzhi_u32(~0, cum_sum); + } + cum_sum += suit_lengths[j]; + } + out.push_back(Node(cards, suits, 0, false)); +#ifdef DEBUG + std::cout << popcnt_u32(cards) << " " + << popcnt_u32(suits[0]) + popcnt_u32(suits[1]) + + popcnt_u32(suits[2]) + popcnt_u32(suits[3]) + << std::endl; + std::cout << cards << " " << suits[0] << " " << suits[1] << " " << suits[2] + << " " << suits[3] << std::endl; +#endif + } + return out; +} + +void ThreadSolver(int size_endgames, vectorNa* outTTable, + const vectorNa* TTable, + const std::vector>& bin_coeffs, + const std::vector& suit_splits, + const std::unordered_map& SuitRanks, + size_t start_id, size_t end_id) { + // takes endgames solved to depth d-1 and returns endgames solved to depth d + // // + std::vector combination; + combination.reserve(size_endgames); + for (int i = 0; i < size_endgames; ++i) { + combination.push_back(i); + } + bool control = true; + int count = 0; + uint32_t cards = 0; + for (int i = 0; i < combination.size(); ++i) { + cards = cards | (1 << combination[i]); + } + while (count < start_id) { + NextColex(combination, 2 * size_endgames); + count++; + } + while (count < end_id && control) { + uint32_t cards = 0; + for (int i = 0; i < combination.size(); ++i) { + cards = cards | (1 << combination[i]); + } + for (int i = 0; i < suit_splits.size(); ++i) { + std::array suit_arr; + suit_arr[0] = bzhi_u32(~0, suit_splits[i] & 0b1111); + uint32_t sum = suit_splits[i] & 0b1111; + for (int j = 1; j < kNumSuits; ++j) { + uint32_t mask = bzhi_u32(~0, sum); + sum += (suit_splits[i] & (0b1111 << (4 * j))) >> 4 * j; + suit_arr[j] = bzhi_u32(~0, sum); + suit_arr[j] = suit_arr[j] ^ mask; + } + Node node(cards, suit_arr, 0, false); + char result = IncrementalAlphaBetaMemoryIso( + &node, 0, size_endgames, 2, TTable, &SuitRanks, bin_coeffs); + outTTable->Set(count, i, result); + } + control = NextColex(combination, 2 * size_endgames); + count++; + } +} +vectorNa RetroSolver(int size_endgames, vectorNa* TTable, + const std::vector>& bin_coeffs,const uint32_t hard_threads) { + // takes endgames solved to depth d-1 and returns endgames solved to depth d + // // + vectorNa outTTable = InitialiseTTable(size_endgames, bin_coeffs); + std::vector suit_splits = GenQuads(size_endgames); + std::unordered_map SuitRanks; + GenSuitRankingsRel(size_endgames - 1, &SuitRanks); + std::vector combination; + combination.reserve(size_endgames); + for (int i = 0; i < size_endgames; ++i) { + combination.push_back(i); + } + uint32_t v_length = (suit_splits.size() >> 1) + 1; + uint32_t min_block_size = 256; + uint32_t num_threads = 1; + uint32_t num_outers = outTTable.GetOuterSize(); + // a haphazard attempt to mitigate false sharing// + for (uint32_t i = hard_threads; i >= 1; i--) { + if ((num_outers * v_length / i) >= min_block_size) { + num_threads = i; + break; + } + } + std::vector threads = {}; + for (int i = 0; i < num_threads; ++i) { + uint32_t block_size = num_outers / num_threads; + uint32_t start_id; + uint32_t end_id; + if (num_threads == 1) { + start_id = 0; + end_id = num_outers; + } else if (i == num_threads - 1) { + start_id = block_size * (num_threads - 1); + end_id = num_outers; + } else { + start_id = block_size * i; + end_id = block_size * (i + 1); + } + threads.emplace_back([&, start_id, end_id]() { + ThreadSolver(size_endgames, &outTTable, TTable, std::ref(bin_coeffs), + std::ref(suit_splits), std::ref(SuitRanks), start_id, + end_id); + }); + } + for (int i = 0; i < num_threads; ++i) { + threads[i].join(); + } + return outTTable; +} + +bool TestRetroSolve(int samples, int depth, uint32_t seed, + const std::vector>& bin_coeffs,const uint32_t hard_threads) { + // Tests endgame solution with TTable vs raw seach + std::vector nodes = GWhistGenerator(samples, seed); + vectorNa v; + for (int i = 1; i <= depth; ++i) { + v = RetroSolver(i, &v, bin_coeffs,hard_threads); + } + std::unordered_map SuitRanks; + GenSuitRankingsRel(depth, &SuitRanks); + for (auto it = nodes.begin(); it != nodes.end(); ++it) { + char abm_unsafe = IncrementalAlphaBetaMemoryIso(&*it, 0, kNumRanks, + 2 * (kNumRanks - depth), &v, + &SuitRanks, bin_coeffs); + char abm_safe = AlphaBeta(&*it, 0, kNumRanks); + if (abm_unsafe != abm_safe) { + return false; + } + } + return true; +} +vectorNa BuildTablebase(const std::vector>& bin_coeffs,const uint32_t hard_threads) { + vectorNa v; + std::cout << "Building Tablebase" + << "\n"; + for (int i = 1; i <= kNumRanks; ++i) { + v = RetroSolver(i, &v, bin_coeffs,hard_threads); + std::cout << "Done " << i << "\n"; + } + std::cout << "Built Tablebase" + << "\n"; + return v; +} +bool TestTablebase(int samples, uint32_t seed, const vectorNa& table_base, + const std::vector>& bin_coeffs) { + std::vector nodes = GWhistGenerator(samples, seed); + std::unordered_map SuitRanks; + GenSuitRankingsRel(kNumRanks, &SuitRanks); + for (auto it = nodes.begin(); it != nodes.end(); ++it) { + char abm_unsafe = IncrementalAlphaBetaMemoryIso( + &*it, 0, kNumRanks, 0, &table_base, &SuitRanks, bin_coeffs); + char abm_safe = AlphaBeta(&*it, 0, kNumRanks); + if (abm_unsafe != abm_safe) { + return false; + } + } + return true; +} +void StoreTTable(const std::string filename, const vectorNa& solution) { + // stores solution into a text file// + std::ofstream file(filename); + for (int i = 0; i < solution.GetOuterSize(); ++i) { + for (int j = 0; j < solution.GetInnerSize(); ++j) { + file.put(solution.GetChar(i, j)); + } + } + file.close(); +} + +bool TestTTableStorage(std::string filename, const vectorNa& v, int depth, + const std::vector>& bin_coeffs) { + // Tests storage fidelity// + StoreTTable(filename, v); + vectorNa new_v = LoadTTable(filename, depth, bin_coeffs); + for (int i = 0; i < v.GetOuterSize(); ++i) { + for (int j = 0; j < v.GetInnerSize(); ++j) { + if (v.GetChar(i, j) != new_v.GetChar(i, j)) { + return false; + } + } + } + return true; +} + +} // namespace german_whist_foregame +} // namespace open_spiel + +int main() { + std::vector> bin_coeffs = + open_spiel::german_whist_foregame::BinCoeffs( + 2 * open_spiel::german_whist_foregame::kNumRanks); + const uint32_t hard_threads = 8;//set this to take advantage of more cores on your machine// + open_spiel::german_whist_foregame::vectorNa tablebase = + open_spiel::german_whist_foregame::BuildTablebase(bin_coeffs,hard_threads); + std::random_device rd; + int num_samples = 100; + if (open_spiel::german_whist_foregame::TestTablebase(num_samples, rd(), + tablebase, bin_coeffs)) { + std::cout << "Tablebase accurate" << std::endl; + } else { + std::cout << "Tablebase inaccurate" << std::endl; + } + std::cout << "Starting Saving Tablebase" << std::endl; + open_spiel::german_whist_foregame::StoreTTable("TTable13.txt", tablebase); + std::cout << "Finished Saving Tablebase" << std::endl; +} diff --git a/open_spiel/games/german_whist_foregame/german_whist_foregame.cc b/open_spiel/games/german_whist_foregame/german_whist_foregame.cc new file mode 100644 index 0000000000..4605a0e69b --- /dev/null +++ b/open_spiel/games/german_whist_foregame/german_whist_foregame.cc @@ -0,0 +1,693 @@ + +#include "open_spiel/games/german_whist_foregame/german_whist_foregame.h" +#include "open_spiel/abseil-cpp/absl/strings/str_cat.h" +#include "open_spiel/game_parameters.h" +#include "open_spiel/observer.h" +#include "open_spiel/policy.h" +#include "open_spiel/spiel.h" +#include "open_spiel/spiel_utils.h" +// define BMI2 only if your system supports BMI2 intrinsics, modify compiler +// flags so that bmi2 instructions are compiled// #define __BMI2__ +#ifdef __BMI2__ +#include +#endif +namespace open_spiel { +namespace german_whist_foregame { + +// set this to the path you expect TTable to be once you have made it so +// recompilation is not necessary// +std::string kTTablePath = ""; + +uint32_t tzcnt_u32(uint32_t a) { return __builtin_ctz(a); } +uint64_t tzcnt_u64(uint64_t a) { return __builtin_ctzll(a); } +uint32_t bzhi_u32(uint32_t a, uint32_t b) { return a & ((1u << b) - 1); } +uint64_t bzhi_u64(uint64_t a, uint64_t b) { return a & ((1ULL << b) - 1); } +uint32_t blsr_u32(uint32_t a) { return (a - 1) & a; } +uint64_t blsr_u64(uint64_t a) { return (a - 1) & a; } +uint32_t popcnt_u32(uint32_t a) { return __builtin_popcount(a); } +uint64_t popcnt_u64(uint64_t a) { return __builtin_popcountll(a); } +// the pext bithack is a lot slower than the bmi2 intrinsic, and even with bmi2 +// support enabled this will not compile down to a pext instruction// +uint64_t pext_u64(uint64_t x, uint64_t m) { +#ifdef __BMI2__ + return _pext_u64(x, m); +#endif +#ifndef __BMI2__ + uint64_t r = 0; + uint64_t s = 0; + uint64_t b = 0; + do { + b = m & 1; + r = r | ((x & b) << s); + s = s + b; + x = x >> 1; + m = m >> 1; + } while (m != 0); + return r; +#endif +} + +bool Triple::operator<(const Triple& triple) const { + return (length < triple.length) || + (length == triple.length && sig < triple.sig); +} + +inline int CardRank(int card, int suit) { + uint64_t card_mask = ((uint64_t)1 << card); + card_mask = (card_mask >> (suit * kNumRanks)); + return tzcnt_u64(card_mask); +} +inline int CardSuit(int card) { + uint64_t card_mask = ((uint64_t)1 << card); + for (int i = 0; i < kNumSuits; ++i) { + if (popcnt_u64(card_mask & kSuitMasks[i]) == 1) { + return i; + } + } + return kNumSuits; +} +std::string CardString(int card) { + int suit = CardSuit(card); + return {kSuitChar[suit], kRankChar[CardRank(card, suit)]}; +} + +std::vector GenQuads(int size_endgames) { + // Generates Suit splittings for endgames of a certain size// + std::vector v; + for (char i = 0; i <= std::min(size_endgames * 2, kNumRanks); ++i) { + int sum = size_endgames * 2 - i; + for (char j = 0; j <= std::min(sum, kNumRanks); ++j) { + for (char k = std::max((int)j, sum - j - kNumRanks); + k <= std::min(sum - j, kNumRanks); ++k) { + char l = sum - j - k; + if (l < k) { + break; + } else { + uint32_t num = 0; + num = num | (i); + num = num | (j << 4); + num = num | (k << 8); + num = num | (l << 12); + v.push_back(num); + } + } + } + } + return v; +} +std::vector> BinCoeffs(uint32_t max_n) { + // tabulates binomial coefficients// + std::vector> C(max_n + 1, + std::vector(max_n + 1)); + for (uint32_t i = 1; i <= max_n; ++i) { + C[0][i] = 0; + } + for (uint32_t i = 0; i <= max_n; ++i) { + C[i][0] = 1; + } + for (uint32_t i = 1; i <= max_n; ++i) { + for (uint32_t j = 1; j <= max_n; ++j) { + C[i][j] = C[i - 1][j] + C[i - 1][j - 1]; + } + } + return C; +} +uint32_t HalfColexer(uint32_t cards, + const std::vector>* bin_coeffs) { + // returns the colexicographical ranking of a combination of indices where the + // the size of the combination is half that of the set of indices// + uint32_t out = 0; + uint32_t count = 0; + while (cards != 0) { + uint32_t ind = tzcnt_u32(cards); + uint32_t val = bin_coeffs->at(ind)[count + 1]; + out += val; + cards = blsr_u32(cards); + count++; + } + return out; +} +void GenSuitRankingsRel(uint32_t size, + std::unordered_map* Ranks) { + // Generates ranking Table for suit splittings for endgames of a certain + // size// + std::vector v = GenQuads(size); + for (uint32_t i = 0; i < v.size(); ++i) { + Ranks->insert({v[i], i}); + } +} + +vectorNa::vectorNa(size_t card_combs, size_t suit_splits, char val) { + data = std::vector(card_combs * ((suit_splits >> 1) + 1), val); + inner_size = (suit_splits >> 1) + 1; + outer_size = card_combs; +} +vectorNa::vectorNa() { + data = {}; + inner_size = 0; + outer_size = 0; +} +size_t vectorNa::size() const { return data.size(); } +size_t vectorNa::GetInnerSize() const { return inner_size; } +size_t vectorNa::GetOuterSize() const { return outer_size; } +char const& vectorNa::operator[](size_t index) const { return data[index]; } +char vectorNa::GetChar(size_t i, size_t j) const { + return data[i * inner_size + j]; +} +void vectorNa::SetChar(size_t i, size_t j, char value) { + data[i * inner_size + j] = value; +} +char vectorNa::Get(size_t i, size_t j) const { + int remainder = j & 0b1; + if (remainder == 0) { + return 0b1111 & data[i * inner_size + (j >> 1)]; + } else { + return ((0b11110000 & data[i * inner_size + (j >> 1)]) >> 4); + } +} +void vectorNa::Set(size_t i, size_t j, char value) { + int remainder = j & 0b1; + if (remainder == 0) { + char datastore = 0b11110000 & data[i * inner_size + (j >> 1)]; + data[i * inner_size + (j >> 1)] = datastore | value; + } else { + char datastore = (0b1111 & data[i * inner_size + (j >> 1)]); + data[i * inner_size + (j >> 1)] = datastore | (value << 4); + } +} +vectorNa InitialiseTTable(int size, + const std::vector>& bin_coeffs) { + // initialises TTable for a certain depth// + size_t suit_size = GenQuads(size).size(); + return vectorNa(bin_coeffs[2 * size][size], suit_size, 0); +} +vectorNa LoadTTable(const std::string filename, int depth, + const std::vector>& bin_coeffs) { + // loads solution from a text file into a vector for use// + std::cout << "Loading Tablebase" + << "\n"; + vectorNa v = InitialiseTTable(depth, bin_coeffs); + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + std::cout << "Failed to load Tablebase" + << "\n"; + std::cout << "Tablebase will be set to all 0" + << "\n"; + file.close(); + return v; + } else { + char c; + for (int i = 0; i < v.GetOuterSize(); ++i) { + for (int j = 0; j < v.GetInnerSize(); ++j) { + file.get(c); + v.SetChar(i, j, c); + } + } + file.close(); + std::cout << "Tablebase Loaded" + << "\n"; + return v; + } +} + +// Default parameters. + +namespace { // namespace +// Facts about the game +const GameType kGameType{ + /*short_name=*/"german_whist_foregame", + /*long_name=*/"german_whist_foregame", + GameType::Dynamics::kSequential, + GameType::ChanceMode::kExplicitStochastic, + GameType::Information::kImperfectInformation, + GameType::Utility::kZeroSum, + GameType::RewardModel::kTerminal, + /*max_num_players=*/2, + /*min_num_players=*/2, + /*provides_information_state_string=*/true, + /*provides_information_state_tensor=*/false, + /*provides_observation_string=*/true, + /*provides_observation_tensor=*/false, +}; + +std::shared_ptr Factory(const GameParameters& params) { + return std::shared_ptr(new GWhistFGame(params)); +} + +REGISTER_SPIEL_GAME(kGameType, Factory); +} // namespace + +GWhistFGame::GWhistFGame(const GameParameters& params) + : Game(kGameType, params) { + bin_coeffs_ = BinCoeffs(2 * kNumRanks); + std::unordered_map temp; + GenSuitRankingsRel(13, &temp); + suit_ranks_ = temp; + ttable_ = LoadTTable(kTTablePath, 13, bin_coeffs_); +}; +std::unique_ptr GWhistFGame::NewInitialState() const { + const auto ptr = + std::dynamic_pointer_cast(shared_from_this()); + return std::make_unique(ptr); +} + +GWhistFState::GWhistFState(std::shared_ptr game) + : State(game) { + player_ = kChancePlayerId; + move_number_ = 0; + trump_ = -1; + deck_ = bzhi_u64(~0, kNumRanks * kNumSuits); + discard_ = 0; + hands_ = {0, 0}; + history_.reserve(78); + ttable_ = &(game->ttable_); + suit_ranks_ = &(game->suit_ranks_); + bin_coeffs_ = &(game->bin_coeffs_); +} +bool GWhistFState::Trick(int lead, int follow) const { + int lead_suit = CardSuit(lead); + int follow_suit = CardSuit(follow); + int lead_rank = CardRank(lead, lead_suit); + int follow_rank = CardRank(follow, follow_suit); + return (lead_suit == follow_suit && lead_rank < follow_rank) || + (lead_suit != follow_suit && follow_suit != trump_); +} +bool GWhistFState::IsTerminal() const { return (popcnt_u64(deck_) == 0); } +uint64_t GWhistFState::EndgameKey(int player_to_move) const { + // generates a 64 bit unsigned int where the first 32 are the suit ownerships + // from the perspective of the opponent using canonical rankings// example: if + // Spade suit is to_move = A3, opp =2, suit = 0b100 least significant part of + // first 32 bits is the trump suit, then the remaining suits ascending length + // order. + uint64_t cards_in_play = hands_[0] | hands_[1]; + std::vector suit_lengths = {}; + int opp = (player_to_move == 0) ? 1 : 0; + // sort trump suits by length,then sig// + for (int i = 0; i < kNumSuits; ++i) { + if (i != trump_) { + uint64_t sig = + pext_u64(hands_[opp] & kSuitMasks[i], cards_in_play & kSuitMasks[i]); + suit_lengths.push_back( + Triple{i, popcnt_u64(kSuitMasks[i] & cards_in_play), sig}); + } + } + std::sort(suit_lengths.begin(), suit_lengths.end()); + std::array hand0; + std::array hand1; + hand0[0] = pext_u64(hands_[0], kSuitMasks[trump_]); + hand1[0] = pext_u64(hands_[1], kSuitMasks[trump_]); + for (int i = 0; i < kNumSuits - 1; ++i) { + hand0[i + 1] = pext_u64(hands_[0], kSuitMasks[suit_lengths[i].index]); + hand1[i + 1] = pext_u64(hands_[1], kSuitMasks[suit_lengths[i].index]); + } + std::array hands_shuffled = {0, 0}; + for (int i = 0; i < kNumSuits; ++i) { + hands_shuffled[0] = hands_shuffled[0] | (hand0[i] << (kNumRanks * i)); + hands_shuffled[1] = hands_shuffled[1] | (hand1[i] << (kNumRanks * i)); + } + uint64_t suit_sig = 0; + suit_sig = popcnt_u64(kSuitMasks[trump_] & cards_in_play); + for (int i = 0; i < kNumSuits - 1; ++i) { + suit_sig = suit_sig | ((uint64_t)suit_lengths[i].length << (4 * (i + 1))); + } + suit_sig = (suit_sig << 32); + cards_in_play = hands_shuffled[0] | hands_shuffled[1]; + uint64_t cards = pext_u64(hands_shuffled[opp], cards_in_play); + uint64_t key = cards | suit_sig; + return key; +} +std::vector GWhistFState::Returns() const { + if (IsTerminal()) { + std::vector out = {0, 0}; + int lead_win = Trick(history_[move_number_ - 3].action, + history_[move_number_ - 2].action); + int player_to_move = (lead_win) ? history_[move_number_ - 3].player + : history_[move_number_ - 2].player; + int opp = (player_to_move == 0) ? 1 : 0; + uint64_t key = EndgameKey(player_to_move); + uint32_t cards = (key & bzhi_u64(~0, 32)); + uint32_t colex = HalfColexer(cards, bin_coeffs_); + uint32_t suits = (key & (~0 ^ bzhi_u64(~0, 32))) >> 32; + uint32_t suit_rank = suit_ranks_->at(suits); + char value = ttable_->Get(colex, suit_rank); + out[player_to_move] = 2 * value - kNumRanks; + out[opp] = -out[player_to_move]; + return out; + } else { + std::vector out = {0, 0}; + return out; + } +} + +int GWhistFState::CurrentPlayer() const { return player_; } + +std::vector> GWhistFState::ChanceOutcomes() const { + std::vector> outcomes; + std::vector legal_actions = LegalActions(); + for (int i = 0; i < legal_actions.size(); ++i) { + std::pair pair; + pair.first = legal_actions[i]; + pair.second = 1.0 / legal_actions.size(); + outcomes.push_back(pair); + } + return outcomes; +} +std::string GWhistFState::ActionToString(Player player, Action move) const { + return CardString(move); +} +std::string GWhistFState::ToString() const { + std::string out; + for (int i = 0; i < history_.size(); ++i) { + out += ActionToString(history_[i].player, history_[i].action); + out += "\n"; + } + return out; +} +std::unique_ptr GWhistFState::Clone() const { + return std::unique_ptr(new GWhistFState(*this)); +} + +std::string GWhistFState::StateToString() const { + // doesnt use history in case of a resampled state with unreconciled history// + std::string out; + uint64_t copy_deck = deck_; + uint64_t copy_discard = discard_; + std::array copy_hands = hands_; + std::vector deck_cards; + std::vector player0_cards; + std::vector player1_cards; + std::vector discard; + while (copy_deck != 0) { + deck_cards.push_back(tzcnt_u64(copy_deck)); + copy_deck = blsr_u64(copy_deck); + } + while (copy_discard != 0) { + discard.push_back(tzcnt_u64(copy_discard)); + copy_discard = blsr_u64(copy_discard); + } + + while (copy_hands[0] != 0) { + player0_cards.push_back(tzcnt_u64(copy_hands[0])); + copy_hands[0] = blsr_u64(copy_hands[0]); + } + while (copy_hands[1] != 0) { + player1_cards.push_back(tzcnt_u64(copy_hands[1])); + copy_hands[1] = blsr_u64(copy_hands[1]); + } + out += "Deck \n"; + for (int i = 0; i < deck_cards.size(); ++i) { + out += CardString(deck_cards[i]) + "\n"; + } + out += "Discard \n"; + for (int i = 0; i < discard.size(); ++i) { + out += CardString(discard[i]) + "\n"; + } + + for (int i = 0; i < 2; ++i) { + out += "Player " + std::to_string(i) + "\n"; + std::vector var; + if (i == 0) { + var = player0_cards; + } else { + var = player1_cards; + } + for (int j = 0; j < var.size(); ++j) { + out += CardString(var[j]) + "\n"; + } + } + return out; +} +std::string GWhistFState::InformationStateString(Player player) const { + // THIS IS WHAT A PLAYER IS SHOWN WHEN PLAYING// + SPIEL_CHECK_TRUE(player >= 0 && player < 2); + std::string p = std::to_string(player) + ","; + std::string cur_hand = ""; + std::string observations = ""; + std::vector v_hand = {}; + uint64_t p_hand = hands_[player]; + while (p_hand != 0) { + v_hand.push_back(tzcnt_u64(p_hand)); + p_hand = blsr_u64(p_hand); + } + std::sort(v_hand.begin(), v_hand.end()); + for (int i = 0; i < v_hand.size(); ++i) { + cur_hand = cur_hand + CardString(v_hand[i]); + cur_hand = cur_hand + ","; + } + cur_hand += "\n"; + for (int i = 2 * kNumRanks; i < history_.size(); ++i) { + int index = (i - 2 * kNumRanks) % 4; + switch (index) { + case 0: + observations = + observations + "c_public:" + CardString(history_[i].action) + ","; + break; + case 1: + observations = observations + "p" + std::to_string(history_[i].player) + + ":" + CardString(history_[i].action) + ","; + break; + case 2: + observations = observations + "p" + std::to_string(history_[i].player) + + ":" + CardString(history_[i].action) + ","; + break; + case 3: + int lead_win = Trick(history_[i - 2].action, history_[i - 1].action); + int loser = ((lead_win) ^ (history_[i - 2].player == 0)) ? 0 : 1; + if (loser == player) { + observations = observations + + "c_observed:" + CardString(history_[i].action) + "\n"; + } else { + observations = observations + "c_unobserved:" + "\n"; + } + break; + } + } + return p + cur_hand + observations; +} +std::unique_ptr GWhistFState::ResampleFromInfostate( + int player_id, std::function rng) const { + // only valid when called from a position where a player can act// + auto resampled_state = std::unique_ptr(new GWhistFState(*this)); + // seeding mt19937// + std::random_device rd; + std::mt19937 gen(rd()); + uint64_t necessary_cards = 0; + for (int i = 2 * kNumRanks; i < history_.size(); i += 4) { + // face up cards from deck// + necessary_cards = (necessary_cards | (uint64_t(1) << history_[i].action)); + } + int move_index = move_number_ - ((kNumRanks * kNumSuits) / 2); + int move_remainder = move_index % 4; + int opp = (player_id == 0) ? 1 : 0; + int recent_faceup = move_number_ - move_remainder; + uint64_t recent_faceup_card = (uint64_t(1) << history_[recent_faceup].action); + // if a face up card from the deck is not in players hand or discard it must + // be in opps unless it is the most recent face up// + necessary_cards = (necessary_cards & + (~(hands_[player_id] | discard_ | recent_faceup_card))); + // sufficient cards are all cards not in players hand,the discard, or the + // recent face up// + uint64_t sufficient_cards = + (bzhi_u64(~0, kNumRanks * kNumSuits) ^ + (hands_[player_id] | discard_ | recent_faceup_card)); + // sufficient_cards are not necessary // + sufficient_cards = (sufficient_cards & (~(necessary_cards))); + // we must now take into account the observation of voids// + std::array when_voided = {0, 0, 0, 0}; + std::array voids = {-1, -1, -1, -1}; + std::vector opp_dealt_hidden; + for (int i = 2 * kNumRanks; i < history_.size(); ++i) { + if (history_[i - 1].player == player_id && history_[i].player == (opp) && + CardSuit(history_[i - 1].action) != CardSuit(history_[i].action)) { + when_voided[CardSuit(history_[i - 1].action)] = i - 1; + } + if (history_[i - 1].player == player_id && history_[i].player == (opp) && + Trick(history_[i - 1].action, history_[i].action)) { + opp_dealt_hidden.push_back(i - 1); + } + if (history_[i - 1].player == (opp) && history_[i].player == (player_id) && + !Trick(history_[i - 1].action, history_[i].action)) { + opp_dealt_hidden.push_back(i - 1); + } + } + // now voids contains the number of hidden cards dealt to opp since it showed + // a void in that suit, i.e the maximum number of cards held in that suit// if + // the suit is unvoided, then this number is -1// + for (int i = 0; i < kNumSuits; ++i) { + if (when_voided[i] != 0) { + voids[i] = 0; + for (int j = 0; j < opp_dealt_hidden.size(); ++j) { + if (opp_dealt_hidden[j] >= when_voided[i]) { + voids[i] += 1; + } + } + } + } + // we now perform a sequence of shuffles to generate a possible opponent hand, + // and make no attempt to reconcile the history with this new deal// + int nec = popcnt_u64(necessary_cards); + for (int i = 0; i < kNumSuits; ++i) { + if (voids[i] != -1 && + popcnt_u64(sufficient_cards & kSuitMasks[i]) > voids[i]) { + uint64_t suit_subset = (sufficient_cards & kSuitMasks[i]); + std::vector temp; + while (suit_subset != 0) { + temp.push_back(tzcnt_u64(suit_subset)); + suit_subset = blsr_u64(suit_subset); + } + std::shuffle(temp.begin(), temp.end(), gen); + sufficient_cards = (sufficient_cards & ~(kSuitMasks[i])); + for (int j = 0; j < voids[i]; ++j) { + sufficient_cards = (sufficient_cards | (uint64_t(1) << temp[j])); + } + } + } + // finally generating a possible hand for opponent// + std::vector hand_vec; + while (sufficient_cards != 0) { + hand_vec.push_back(tzcnt_u64(sufficient_cards)); + sufficient_cards = blsr_u64(sufficient_cards); + } + std::shuffle(hand_vec.begin(), hand_vec.end(), gen); + uint64_t suff_hand = 0; + uint64_t opp_hand = 0; + for (int i = 0; i < popcnt_u64(hands_[opp]) - nec; ++i) { + suff_hand = suff_hand | (uint64_t(1) << hand_vec[i]); + } + opp_hand = suff_hand | necessary_cards; + resampled_state->hands_[opp] = opp_hand; + resampled_state->deck_ = + bzhi_u64(~0, kNumRanks * kNumSuits) ^ + (discard_ | opp_hand | hands_[player_id] | recent_faceup_card); + return resampled_state; +} +std::string GWhistFState::ObservationString(Player player) const { + // note this is a lie, this is not the observation state string but it is used + // for ISMCTS to label nodes// + SPIEL_CHECK_TRUE(player >= 0 && player < 2); + std::string p = "p" + std::to_string(player) + ","; + std::string cur_hand = ""; + std::string public_info = ""; + uint64_t p_hand = hands_[player]; + std::vector v_hand = {}; + while (p_hand != 0) { + v_hand.push_back(tzcnt_u64(p_hand)); + p_hand = blsr_u64(p_hand); + } + std::sort(v_hand.begin(), v_hand.end()); + for (int i = 0; i < v_hand.size(); ++i) { + cur_hand = cur_hand + CardString(v_hand[i]) + ","; + } + for (int i = 2 * kNumRanks; i < history_.size(); ++i) { + int index = (i - 2 * kNumRanks) % 4; + if (index != 3) { + public_info = public_info + std::to_string(history_[i].player) + ":" + + CardString(history_[i].action) + ","; + } + } + return p + cur_hand + public_info; +} + +std::vector GWhistFState::LegalActions() const { + std::vector actions; + if (IsTerminal()) return {}; + if (IsChanceNode()) { + actions.reserve(popcnt_u64(deck_)); + uint64_t copy_deck = deck_; + while (copy_deck != 0) { + actions.push_back(tzcnt_u64(copy_deck)); + copy_deck = blsr_u64(copy_deck); + } + } else { + // lead// + actions.reserve(kNumRanks); + if (history_.back().player == kChancePlayerId) { + uint64_t copy_hand = hands_[player_]; + while (copy_hand != 0) { + actions.push_back(tzcnt_u64(copy_hand)); + copy_hand = blsr_u64(copy_hand); + } + } + + // follow// + else { + uint64_t copy_hand = + hands_[player_] & kSuitMasks[CardSuit(history_.back().action)]; + if (copy_hand == 0) { + copy_hand = hands_[player_]; + } + while (copy_hand != 0) { + actions.push_back(tzcnt_u64(copy_hand)); + copy_hand = blsr_u64(copy_hand); + } + } + } + return actions; +} + +void GWhistFState::DoApplyAction(Action move) { + // initial deal// + int player_start = player_; + if (move_number_ < (kNumSuits * kNumRanks) / 2) { + hands_[move_number_ % 2] = + (hands_[move_number_ % 2] | ((uint64_t)1 << move)); + deck_ = (deck_ ^ ((uint64_t)1 << move)); + } else if (move_number_ == (kNumSuits * kNumRanks / 2)) { + trump_ = CardSuit(move); + deck_ = (deck_ ^ ((uint64_t)1 << move)); + player_ = 0; + } + // cardplay// + else if (move_number_ > (kNumSuits * kNumRanks) / 2) { + int move_index = (move_number_ - ((kNumSuits * kNumRanks) / 2)) % 4; + switch (move_index) { + bool lead_win; + int winner; + int loser; + case 0: + // revealing face up card// + deck_ = (deck_ ^ ((uint64_t)1 << move)); + lead_win = Trick(history_[move_number_ - 3].action, + history_[move_number_ - 2].action); + winner = + ((lead_win) ^ (history_[move_number_ - 3].player == 0)) ? 1 : 0; + player_ = winner; + break; + case 1: + // establishing lead// + discard_ = (discard_ | ((uint64_t)1 << move)); + hands_[player_] = (hands_[player_] ^ ((uint64_t)1 << move)); + (player_ == 0) ? player_ = 1 : player_ = 0; + break; + case 2: + // following and awarding face up// + discard_ = (discard_ | ((uint64_t)1 << move)); + hands_[player_] = (hands_[player_] ^ ((uint64_t)1 << move)); + lead_win = Trick(history_[move_number_ - 1].action, move); + winner = + ((lead_win) ^ (history_[move_number_ - 1].player == 0)) ? 1 : 0; + hands_[winner] = (hands_[winner] | + ((uint64_t)1 << history_[move_number_ - 2].action)); + player_ = kChancePlayerId; + break; + case 3: + // awarding face down// + deck_ = (deck_ ^ ((uint64_t)1 << move)); + lead_win = Trick(history_[move_number_ - 2].action, + history_[move_number_ - 1].action); + loser = ((lead_win) ^ (history_[move_number_ - 2].player == 0)) ? 0 : 1; + hands_[loser] = (hands_[loser] | ((uint64_t)1 << move)); + if (IsTerminal()) { + player_ = kTerminalPlayerId; + } + break; + } + } +#ifdef DEBUG + std::cout << ActionToString(player_start, move) << std::endl; + std::cout << move << std::endl; +#endif +} + +} // namespace german_whist_foregame +} // namespace open_spiel diff --git a/open_spiel/games/german_whist_foregame/german_whist_foregame.h b/open_spiel/games/german_whist_foregame/german_whist_foregame.h new file mode 100644 index 0000000000..5bdf053d3e --- /dev/null +++ b/open_spiel/games/german_whist_foregame/german_whist_foregame.h @@ -0,0 +1,134 @@ +#ifndef OPEN_SPIEL_GAMES_GERMAN_WHIST_FOREGAME_H +#define OPEN_SPIEL_GAMES_GERMAN_WHIST_FOREGAME_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "open_spiel/policy.h" +#include "open_spiel/spiel.h" +#include "open_spiel/spiel_utils.h" + +// The imperfect information part of 2 player whist variant +// https://en.wikipedia.org/wiki/German_Whist + +namespace open_spiel { +namespace german_whist_foregame { + + + +class GWhistFGame; +class GWhistFObserver; + +inline constexpr int kNumRanks = 13; +inline constexpr int kNumSuits = 4; +inline constexpr char kRankChar[] = "AKQJT98765432"; +inline constexpr char kSuitChar[] = "CDHS"; + +extern std::string kTTablePath; + +// Reimplementing bmi2 intrinsics with bit operations that will work on all platforms// +uint32_t tzcnt_u32(uint32_t a); +uint64_t tzcnt_u64(uint64_t a); +uint32_t bzhi_u32(uint32_t a,uint32_t b); +uint64_t bzhi_u64(uint64_t a,uint64_t b); +uint32_t blsr_u32(uint32_t a); +uint64_t blsr_u64(uint64_t a); +uint32_t popcnt_u32(uint32_t a); +uint64_t popcnt_u64(uint64_t a); +uint64_t pext_u64(uint64_t a,uint64_t b); + +// containers of cards are 64 bits,with the least significant 52bits being the suits CDHS,with the least sig bit of each suit being the highest rank card// +// this container of masks is used to extract only the cards from a suit// +inline const std::array kSuitMasks = { bzhi_u64(~0,kNumRanks),bzhi_u64(~0,2 * kNumRanks) ^ bzhi_u64(~0,kNumRanks),bzhi_u64(~0,3 * kNumRanks) ^ bzhi_u64(~0,2 * kNumRanks),bzhi_u64(~0,4 * kNumRanks) ^ bzhi_u64(~0,3 * kNumRanks) }; + + +struct Triple{ + char index; + char length; + uint32_t sig; + bool operator<(const Triple& triple) const; +}; +std::vector GenQuads(int size_endgames); +std::vector> BinCoeffs(uint32_t max_n); +uint32_t HalfColexer(uint32_t cards,const std::vector>* bin_coeffs); +void GenSuitRankingsRel(uint32_t size,std::unordered_map* Ranks); +class vectorNa{ +private: + std::vector data; + size_t inner_size; + size_t outer_size; +public: + vectorNa(size_t card_combs,size_t suit_splits,char val); + vectorNa(); + size_t size()const; + size_t GetInnerSize()const; + size_t GetOuterSize()const; + char const& operator[](size_t index)const; + char GetChar(size_t i,size_t j)const; + void SetChar(size_t i,size_t j,char value); + char Get(size_t i,size_t j) const; + void Set(size_t i,size_t j,char value); +}; +vectorNa InitialiseTTable(int size,const std::vector>& bin_coeffs); +vectorNa LoadTTable(const std::string filename,int depth,const std::vector>& bin_coeffs); +class GWhistFGame : public Game { +public: + explicit GWhistFGame(const GameParameters& params); + int NumDistinctActions() const override { return kNumRanks*kNumSuits; } + std::unique_ptr NewInitialState() const override; + int MaxChanceOutcomes() const override { return kNumRanks*kNumSuits ; } + int NumPlayers() const override { return num_players_; } + double MinUtility() const override {return -kNumRanks;}; + double MaxUtility() const override {return kNumRanks;}; + absl::optional UtilitySum() const override { return 0; }; + int MaxGameLength() const override{return kNumRanks*(kNumSuits+2);}; + int MaxChanceNodesInHistory() const override{return kNumRanks*kNumSuits;}; + vectorNa ttable_; + std::unordered_map suit_ranks_; + std::vector>bin_coeffs_; +private: + // Number of players. + int num_players_ = 2; +}; +class GWhistFState : public State { +public: + explicit GWhistFState(std::shared_ptr game); + GWhistFState(const GWhistFState&) = default; + Player CurrentPlayer() const override; + std::string ActionToString(Player player, Action move) const override; + std::string ToString() const override; + bool IsTerminal() const override; + std::vector Returns() const override; + std::unique_ptr Clone() const override; + ActionsAndProbs ChanceOutcomes() const override; + std::vector LegalActions() const override; + std::string InformationStateString(Player player) const override; + std::string ObservationString(Player player) const override; + std::unique_ptr ResampleFromInfostate(int player_id,std::function rng) const override; + std::string StateToString() const; + uint64_t EndgameKey(int player_to_move) const; +protected: + void DoApplyAction(Action move) override; +private: + uint64_t deck_; + uint64_t discard_; + const vectorNa* ttable_; + const std::unordered_map* suit_ranks_; + const std::vector>* bin_coeffs_; + std::array hands_; + int player_; + int trump_; + bool Trick(int lead,int follow) const; +}; +}// namespace german_whist_foregame +}// namespace open_spiel + + +#endif OPEN_SPIEL_GAMES_GERMAN_WHIST_FOREGAME_H diff --git a/open_spiel/games/german_whist_foregame/german_whist_foregame_test.cc b/open_spiel/games/german_whist_foregame/german_whist_foregame_test.cc new file mode 100644 index 0000000000..b73a687d54 --- /dev/null +++ b/open_spiel/games/german_whist_foregame/german_whist_foregame_test.cc @@ -0,0 +1,32 @@ + + +#include "open_spiel/games/german_whist_foregame/german_whist_foregame.h" + +#include "open_spiel/algorithms/get_all_states.h" +#include "open_spiel/policy.h" +#include "open_spiel/spiel_utils.h" +#include "open_spiel/tests/basic_tests.h" + +namespace open_spiel { +namespace german_whist_foregame { +namespace { + +namespace testing = open_spiel::testing; + + +void BasicGermanWhistForegameTests() { + testing::LoadGameTest("german_whist_foregame"); + //testing::ChanceOutcomesTest(*LoadGame("german_whist_foregame")); + testing::RandomSimTest(*LoadGame("german_whist_foregame"),100,false,true); +} + + + +} // namespace +} // namespace GermanWhistForegame_ +} // namespace open_spiel + +int main(int argc, char **argv) { + open_spiel::german_whist_foregame::BasicGermanWhistForegameTests(); + //open_spiel::testing::ResampleInfostateTest(*open_spiel::LoadGame("german_whist_foregame"),*num_sims=*10); +} diff --git a/open_spiel/integration_tests/playthroughs/german_whist_foregame.txt b/open_spiel/integration_tests/playthroughs/german_whist_foregame.txt new file mode 100644 index 0000000000..f068c1fd86 --- /dev/null +++ b/open_spiel/integration_tests/playthroughs/german_whist_foregame.txt @@ -0,0 +1,905 @@ +game: german_whist_foregame + +GameType.chance_mode = ChanceMode.EXPLICIT_STOCHASTIC +GameType.dynamics = Dynamics.SEQUENTIAL +GameType.information = Information.IMPERFECT_INFORMATION +GameType.long_name = "german_whist_foregame" +GameType.max_num_players = 2 +GameType.min_num_players = 2 +GameType.parameter_specification = [] +GameType.provides_information_state_string = True +GameType.provides_information_state_tensor = False +GameType.provides_observation_string = True +GameType.provides_observation_tensor = False +GameType.provides_factored_observation_string = False +GameType.reward_model = RewardModel.TERMINAL +GameType.short_name = "german_whist_foregame" +GameType.utility = Utility.ZERO_SUM + +NumDistinctActions() = 52 +PolicyTensorShape() = [52] +MaxChanceOutcomes() = 52 +GetParameters() = {} +NumPlayers() = 2 +MinUtility() = -13.0 +MaxUtility() = 13.0 +UtilitySum() = 0.0 +MaxGameLength() = 78 +ToString() = "german_whist_foregame()" + +# State 0 +IsTerminal() = False +History() = [] +HistoryString() = "" +IsChanceNode() = True +IsSimultaneousNode() = False +CurrentPlayer() = -1 +InformationStateString(0) = "0,\n" +InformationStateString(1) = "1,\n" +ObservationString(0) = "p0," +ObservationString(1) = "p1," +ChanceOutcomes() = [(0,0.0192308), (1,0.0192308), (2,0.0192308), (3,0.0192308), (4,0.0192308), (5,0.0192308), (6,0.0192308), (7,0.0192308), (8,0.0192308), (9,0.0192308), (10,0.0192308), (11,0.0192308), (12,0.0192308), (13,0.0192308), (14,0.0192308), (15,0.0192308), (16,0.0192308), (17,0.0192308), (18,0.0192308), (19,0.0192308), (20,0.0192308), (21,0.0192308), (22,0.0192308), (23,0.0192308), (24,0.0192308), (25,0.0192308), (26,0.0192308), (27,0.0192308), (28,0.0192308), (29,0.0192308), (30,0.0192308), (31,0.0192308), (32,0.0192308), (33,0.0192308), (34,0.0192308), (35,0.0192308), (36,0.0192308), (37,0.0192308), (38,0.0192308), (39,0.0192308), (40,0.0192308), (41,0.0192308), (42,0.0192308), (43,0.0192308), (44,0.0192308), (45,0.0192308), (46,0.0192308), (47,0.0192308), (48,0.0192308), (49,0.0192308), (50,0.0192308), (51,0.0192308)] +LegalActions() = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] +StringLegalActions() = ["CA", "CK", "CQ", "CJ", "CT", "C9", "C8", "C7", "C6", "C5", "C4", "C3", "C2", "DA", "DK", "DQ", "DJ", "DT", "D9", "D8", "D7", "D6", "D5", "D4", "D3", "D2", "HA", "HK", "HQ", "HJ", "HT", "H9", "H8", "H7", "H6", "H5", "H4", "H3", "H2", "SA", "SK", "SQ", "SJ", "ST", "S9", "S8", "S7", "S6", "S5", "S4", "S3", "S2"] + +# Apply action "SA" +action: 39 + +# State 1 +# SA +IsTerminal() = False +History() = [39] +HistoryString() = "39" +IsChanceNode() = True +IsSimultaneousNode() = False +CurrentPlayer() = -1 +InformationStateString(0) = "0,SA,\n" +InformationStateString(1) = "1,\n" +ObservationString(0) = "p0,SA," +ObservationString(1) = "p1," +ChanceOutcomes() = [(0,0.0196078), (1,0.0196078), (2,0.0196078), (3,0.0196078), (4,0.0196078), (5,0.0196078), (6,0.0196078), (7,0.0196078), (8,0.0196078), (9,0.0196078), (10,0.0196078), (11,0.0196078), (12,0.0196078), (13,0.0196078), (14,0.0196078), (15,0.0196078), (16,0.0196078), (17,0.0196078), (18,0.0196078), (19,0.0196078), (20,0.0196078), (21,0.0196078), (22,0.0196078), (23,0.0196078), (24,0.0196078), (25,0.0196078), (26,0.0196078), (27,0.0196078), (28,0.0196078), (29,0.0196078), (30,0.0196078), (31,0.0196078), (32,0.0196078), (33,0.0196078), (34,0.0196078), (35,0.0196078), (36,0.0196078), (37,0.0196078), (38,0.0196078), (40,0.0196078), (41,0.0196078), (42,0.0196078), (43,0.0196078), (44,0.0196078), (45,0.0196078), (46,0.0196078), (47,0.0196078), (48,0.0196078), (49,0.0196078), (50,0.0196078), (51,0.0196078)] +LegalActions() = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51] +StringLegalActions() = ["CA", "CK", "CQ", "CJ", "CT", "C9", "C8", "C7", "C6", "C5", "C4", "C3", "C2", "DA", "DK", "DQ", "DJ", "DT", "D9", "D8", "D7", "D6", "D5", "D4", "D3", "D2", "HA", "HK", "HQ", "HJ", "HT", "H9", "H8", "H7", "H6", "H5", "H4", "H3", "H2", "SK", "SQ", "SJ", "ST", "S9", "S8", "S7", "S6", "S5", "S4", "S3", "S2"] + +# Apply action "CJ" +action: 3 + +# State 2 +# Apply action "C3" +action: 11 + +# State 3 +# Apply action "H7" +action: 33 + +# State 4 +# Apply action "H9" +action: 31 + +# State 5 +# Apply action "SJ" +action: 42 + +# State 6 +# Apply action "H3" +action: 37 + +# State 7 +# Apply action "CK" +action: 1 + +# State 8 +# Apply action "H5" +action: 35 + +# State 9 +# Apply action "D7" +action: 20 + +# State 10 +# Apply action "D5" +action: 22 + +# State 11 +# Apply action "DT" +action: 17 + +# State 12 +# Apply action "D8" +action: 19 + +# State 13 +# Apply action "H4" +action: 36 + +# State 14 +# Apply action "SQ" +action: 41 + +# State 15 +# Apply action "C9" +action: 5 + +# State 16 +# Apply action "DQ" +action: 15 + +# State 17 +# Apply action "HT" +action: 30 + +# State 18 +# Apply action "D3" +action: 24 + +# State 19 +# Apply action "HQ" +action: 28 + +# State 20 +# Apply action "S5" +action: 48 + +# State 21 +# Apply action "SK" +action: 40 + +# State 22 +# Apply action "HJ" +action: 29 + +# State 23 +# Apply action "ST" +action: 43 + +# State 24 +# Apply action "H2" +action: 38 + +# State 25 +# Apply action "C4" +action: 10 + +# State 26 +# Apply action "S8" +action: 45 + +# State 27 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "0,C3,DQ,D8,D5,D3,HJ,H9,H5,H3,H2,SA,SQ,S5,\nc_public:S8," +InformationStateString(1) = "1,CK,CJ,C9,C4,DT,D7,HQ,HT,H7,H4,SK,SJ,ST,\nc_public:S8," +ObservationString(0) = "p0,C3,DQ,D8,D5,D3,HJ,H9,H5,H3,H2,SA,SQ,S5,-1:S8," +ObservationString(1) = "p1,CK,CJ,C9,C4,DT,D7,HQ,HT,H7,H4,SK,SJ,ST,-1:S8," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [11, 15, 19, 22, 24, 29, 31, 35, 37, 38, 39, 41, 48] +StringLegalActions() = ["C3", "DQ", "D8", "D5", "D3", "HJ", "H9", "H5", "H3", "H2", "SA", "SQ", "S5"] + +# Apply action "H3" +action: 37 + +# State 28 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "0,C3,DQ,D8,D5,D3,HJ,H9,H5,H2,SA,SQ,S5,\nc_public:S8,p0:H3," +InformationStateString(1) = "1,CK,CJ,C9,C4,DT,D7,HQ,HT,H7,H4,SK,SJ,ST,\nc_public:S8,p0:H3," +ObservationString(0) = "p0,C3,DQ,D8,D5,D3,HJ,H9,H5,H2,SA,SQ,S5,-1:S8,0:H3," +ObservationString(1) = "p1,CK,CJ,C9,C4,DT,D7,HQ,HT,H7,H4,SK,SJ,ST,-1:S8,0:H3," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [28, 30, 33, 36] +StringLegalActions() = ["HQ", "HT", "H7", "H4"] + +# Apply action "HQ" +action: 28 + +# State 29 +# Apply action "D4" +action: 23 + +# State 30 +# Apply action "C2" +action: 12 + +# State 31 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "0,C3,DQ,D8,D5,D4,D3,HJ,H9,H5,H2,SA,SQ,S5,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2," +InformationStateString(1) = "1,CK,CJ,C9,C4,DT,D7,HT,H7,H4,SK,SJ,ST,S8,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2," +ObservationString(0) = "p0,C3,DQ,D8,D5,D4,D3,HJ,H9,H5,H2,SA,SQ,S5,-1:S8,0:H3,1:HQ,-1:C2," +ObservationString(1) = "p1,CK,CJ,C9,C4,DT,D7,HT,H7,H4,SK,SJ,ST,S8,-1:S8,0:H3,1:HQ,-1:C2," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [1, 3, 5, 10, 17, 20, 30, 33, 36, 40, 42, 43, 45] +StringLegalActions() = ["CK", "CJ", "C9", "C4", "DT", "D7", "HT", "H7", "H4", "SK", "SJ", "ST", "S8"] + +# Apply action "C4" +action: 10 + +# State 32 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "0,C3,DQ,D8,D5,D4,D3,HJ,H9,H5,H2,SA,SQ,S5,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4," +InformationStateString(1) = "1,CK,CJ,C9,DT,D7,HT,H7,H4,SK,SJ,ST,S8,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4," +ObservationString(0) = "p0,C3,DQ,D8,D5,D4,D3,HJ,H9,H5,H2,SA,SQ,S5,-1:S8,0:H3,1:HQ,-1:C2,1:C4," +ObservationString(1) = "p1,CK,CJ,C9,DT,D7,HT,H7,H4,SK,SJ,ST,S8,-1:S8,0:H3,1:HQ,-1:C2,1:C4," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [11] +StringLegalActions() = ["C3"] + +# Apply action "C3" +action: 11 + +# State 33 +# Apply action "HA" +action: 26 + +# State 34 +# Apply action "DJ" +action: 16 + +# State 35 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +# C3 +# HA +# DJ +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "0,DQ,D8,D5,D4,D3,HA,HJ,H9,H5,H2,SA,SQ,S5,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4,p0:C3,c_observed:HA\nc_public:DJ," +InformationStateString(1) = "1,CK,CJ,C9,C2,DT,D7,HT,H7,H4,SK,SJ,ST,S8,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4,p0:C3,c_unobserved:\nc_public:DJ," +ObservationString(0) = "p0,DQ,D8,D5,D4,D3,HA,HJ,H9,H5,H2,SA,SQ,S5,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ," +ObservationString(1) = "p1,CK,CJ,C9,C2,DT,D7,HT,H7,H4,SK,SJ,ST,S8,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [1, 3, 5, 12, 17, 20, 30, 33, 36, 40, 42, 43, 45] +StringLegalActions() = ["CK", "CJ", "C9", "C2", "DT", "D7", "HT", "H7", "H4", "SK", "SJ", "ST", "S8"] + +# Apply action "H7" +action: 33 + +# State 36 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +# C3 +# HA +# DJ +# H7 +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "0,DQ,D8,D5,D4,D3,HA,HJ,H9,H5,H2,SA,SQ,S5,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4,p0:C3,c_observed:HA\nc_public:DJ,p1:H7," +InformationStateString(1) = "1,CK,CJ,C9,C2,DT,D7,HT,H4,SK,SJ,ST,S8,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4,p0:C3,c_unobserved:\nc_public:DJ,p1:H7," +ObservationString(0) = "p0,DQ,D8,D5,D4,D3,HA,HJ,H9,H5,H2,SA,SQ,S5,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7," +ObservationString(1) = "p1,CK,CJ,C9,C2,DT,D7,HT,H4,SK,SJ,ST,S8,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [26, 29, 31, 35, 38] +StringLegalActions() = ["HA", "HJ", "H9", "H5", "H2"] + +# Apply action "HJ" +action: 29 + +# State 37 +# Apply action "S3" +action: 50 + +# State 38 +# Apply action "H8" +action: 32 + +# State 39 +# Apply action "S5" +action: 48 + +# State 40 +# Apply action "S8" +action: 45 + +# State 41 +# Apply action "DK" +action: 14 + +# State 42 +# Apply action "C6" +action: 8 + +# State 43 +# Apply action "ST" +action: 43 + +# State 44 +# Apply action "SQ" +action: 41 + +# State 45 +# Apply action "DA" +action: 13 + +# State 46 +# Apply action "D6" +action: 21 + +# State 47 +# Apply action "D5" +action: 22 + +# State 48 +# Apply action "DA" +action: 13 + +# State 49 +# Apply action "S9" +action: 44 + +# State 50 +# Apply action "C8" +action: 6 + +# State 51 +# Apply action "DT" +action: 17 + +# State 52 +# Apply action "D4" +action: 23 + +# State 53 +# Apply action "CQ" +action: 2 + +# State 54 +# Apply action "C5" +action: 9 + +# State 55 +# Apply action "H4" +action: 36 + +# State 56 +# Apply action "H5" +action: 35 + +# State 57 +# Apply action "CT" +action: 4 + +# State 58 +# Apply action "S4" +action: 49 + +# State 59 +# Apply action "C6" +action: 8 + +# State 60 +# Apply action "CK" +action: 1 + +# State 61 +# Apply action "C7" +action: 7 + +# State 62 +# Apply action "D9" +action: 18 + +# State 63 +# Apply action "C8" +action: 6 + +# State 64 +# Apply action "C5" +action: 9 + +# State 65 +# Apply action "CA" +action: 0 + +# State 66 +# Apply action "HK" +action: 27 + +# State 67 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +# C3 +# HA +# DJ +# H7 +# HJ +# S3 +# H8 +# S5 +# S8 +# DK +# C6 +# ST +# SQ +# DA +# D6 +# D5 +# DA +# S9 +# C8 +# DT +# D4 +# CQ +# C5 +# H4 +# H5 +# CT +# S4 +# C6 +# CK +# C7 +# D9 +# C8 +# C5 +# CA +# HK +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "0,CA,CQ,C7,DK,DQ,DJ,D8,D3,HA,H9,H2,SA,S9,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4,p0:C3,c_observed:HA\nc_public:DJ,p1:H7,p0:HJ,c_unobserved:\nc_public:H8,p0:S5,p1:S8,c_observed:DK\nc_public:C6,p1:ST,p0:SQ,c_unobserved:\nc_public:D6,p0:D5,p1:DA,c_observed:S9\nc_public:C8,p1:DT,p0:D4,c_observed:CQ\nc_public:C5,p1:H4,p0:H5,c_unobserved:\nc_public:S4,p0:C6,p1:CK,c_observed:C7\nc_public:D9,p1:C8,p0:C5,c_observed:CA\nc_public:HK," +InformationStateString(1) = "1,CJ,CT,C9,C2,D9,D7,D6,HT,H8,SK,SJ,S4,S3,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4,p0:C3,c_unobserved:\nc_public:DJ,p1:H7,p0:HJ,c_observed:S3\nc_public:H8,p0:S5,p1:S8,c_unobserved:\nc_public:C6,p1:ST,p0:SQ,c_observed:DA\nc_public:D6,p0:D5,p1:DA,c_unobserved:\nc_public:C8,p1:DT,p0:D4,c_unobserved:\nc_public:C5,p1:H4,p0:H5,c_observed:CT\nc_public:S4,p0:C6,p1:CK,c_unobserved:\nc_public:D9,p1:C8,p0:C5,c_unobserved:\nc_public:HK," +ObservationString(0) = "p0,CA,CQ,C7,DK,DQ,DJ,D8,D3,HA,H9,H2,SA,S9,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK," +ObservationString(1) = "p1,CJ,CT,C9,C2,D9,D7,D6,HT,H8,SK,SJ,S4,S3,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [3, 4, 5, 12, 18, 20, 21, 30, 32, 40, 42, 49, 50] +StringLegalActions() = ["CJ", "CT", "C9", "C2", "D9", "D7", "D6", "HT", "H8", "SK", "SJ", "S4", "S3"] + +# Apply action "CJ" +action: 3 + +# State 68 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +# C3 +# HA +# DJ +# H7 +# HJ +# S3 +# H8 +# S5 +# S8 +# DK +# C6 +# ST +# SQ +# DA +# D6 +# D5 +# DA +# S9 +# C8 +# DT +# D4 +# CQ +# C5 +# H4 +# H5 +# CT +# S4 +# C6 +# CK +# C7 +# D9 +# C8 +# C5 +# CA +# HK +# CJ +IsTerminal() = False +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27, 3] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27, 3" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "0,CA,CQ,C7,DK,DQ,DJ,D8,D3,HA,H9,H2,SA,S9,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4,p0:C3,c_observed:HA\nc_public:DJ,p1:H7,p0:HJ,c_unobserved:\nc_public:H8,p0:S5,p1:S8,c_observed:DK\nc_public:C6,p1:ST,p0:SQ,c_unobserved:\nc_public:D6,p0:D5,p1:DA,c_observed:S9\nc_public:C8,p1:DT,p0:D4,c_observed:CQ\nc_public:C5,p1:H4,p0:H5,c_unobserved:\nc_public:S4,p0:C6,p1:CK,c_observed:C7\nc_public:D9,p1:C8,p0:C5,c_observed:CA\nc_public:HK,p1:CJ," +InformationStateString(1) = "1,CT,C9,C2,D9,D7,D6,HT,H8,SK,SJ,S4,S3,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4,p0:C3,c_unobserved:\nc_public:DJ,p1:H7,p0:HJ,c_observed:S3\nc_public:H8,p0:S5,p1:S8,c_unobserved:\nc_public:C6,p1:ST,p0:SQ,c_observed:DA\nc_public:D6,p0:D5,p1:DA,c_unobserved:\nc_public:C8,p1:DT,p0:D4,c_unobserved:\nc_public:C5,p1:H4,p0:H5,c_observed:CT\nc_public:S4,p0:C6,p1:CK,c_unobserved:\nc_public:D9,p1:C8,p0:C5,c_unobserved:\nc_public:HK,p1:CJ," +ObservationString(0) = "p0,CA,CQ,C7,DK,DQ,DJ,D8,D3,HA,H9,H2,SA,S9,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK,1:CJ," +ObservationString(1) = "p1,CT,C9,C2,D9,D7,D6,HT,H8,SK,SJ,S4,S3,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK,1:CJ," +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [0, 2, 7] +StringLegalActions() = ["CA", "CQ", "C7"] + +# Apply action "C7" +action: 7 + +# State 69 +# Apply action "S7" +action: 46 + +# State 70 +# Apply action "S6" +action: 47 + +# State 71 +# Apply action "HK" +action: 27 + +# State 72 +# Apply action "H9" +action: 31 + +# State 73 +# Apply action "S2" +action: 51 + +# State 74 +# Apply action "D2" +action: 25 + +# State 75 +# Apply action "SK" +action: 40 + +# State 76 +# Apply action "S9" +action: 44 + +# State 77 +# Apply action "H6" +action: 34 + +# State 78 +# SA +# CJ +# C3 +# H7 +# H9 +# SJ +# H3 +# CK +# H5 +# D7 +# D5 +# DT +# D8 +# H4 +# SQ +# C9 +# DQ +# HT +# D3 +# HQ +# S5 +# SK +# HJ +# ST +# H2 +# C4 +# S8 +# H3 +# HQ +# D4 +# C2 +# C4 +# C3 +# HA +# DJ +# H7 +# HJ +# S3 +# H8 +# S5 +# S8 +# DK +# C6 +# ST +# SQ +# DA +# D6 +# D5 +# DA +# S9 +# C8 +# DT +# D4 +# CQ +# C5 +# H4 +# H5 +# CT +# S4 +# C6 +# CK +# C7 +# D9 +# C8 +# C5 +# CA +# HK +# CJ +# C7 +# S7 +# S6 +# HK +# H9 +# S2 +# D2 +# SK +# S9 +# H6 +IsTerminal() = True +History() = [39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27, 3, 7, 46, 47, 27, 31, 51, 25, 40, 44, 34] +HistoryString() = "39, 3, 11, 33, 31, 42, 37, 1, 35, 20, 22, 17, 19, 36, 41, 5, 15, 30, 24, 28, 48, 40, 29, 43, 38, 10, 45, 37, 28, 23, 12, 10, 11, 26, 16, 33, 29, 50, 32, 48, 45, 14, 8, 43, 41, 13, 21, 22, 13, 44, 6, 17, 23, 2, 9, 36, 35, 4, 49, 8, 1, 7, 18, 6, 9, 0, 27, 3, 7, 46, 47, 27, 31, 51, 25, 40, 44, 34" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = -4 +InformationStateString(0) = "0,CA,CQ,DK,DQ,DJ,D8,D3,HA,H6,H2,SA,S7,S2,\nc_public:S8,p0:H3,p1:HQ,c_observed:D4\nc_public:C2,p1:C4,p0:C3,c_observed:HA\nc_public:DJ,p1:H7,p0:HJ,c_unobserved:\nc_public:H8,p0:S5,p1:S8,c_observed:DK\nc_public:C6,p1:ST,p0:SQ,c_unobserved:\nc_public:D6,p0:D5,p1:DA,c_observed:S9\nc_public:C8,p1:DT,p0:D4,c_observed:CQ\nc_public:C5,p1:H4,p0:H5,c_unobserved:\nc_public:S4,p0:C6,p1:CK,c_observed:C7\nc_public:D9,p1:C8,p0:C5,c_observed:CA\nc_public:HK,p1:CJ,p0:C7,c_observed:S7\nc_public:S6,p1:HK,p0:H9,c_observed:S2\nc_public:D2,p1:SK,p0:S9,c_observed:H6\n" +InformationStateString(1) = "1,CT,C9,C2,D9,D7,D6,D2,HT,H8,SJ,S6,S4,S3,\nc_public:S8,p0:H3,p1:HQ,c_unobserved:\nc_public:C2,p1:C4,p0:C3,c_unobserved:\nc_public:DJ,p1:H7,p0:HJ,c_observed:S3\nc_public:H8,p0:S5,p1:S8,c_unobserved:\nc_public:C6,p1:ST,p0:SQ,c_observed:DA\nc_public:D6,p0:D5,p1:DA,c_unobserved:\nc_public:C8,p1:DT,p0:D4,c_unobserved:\nc_public:C5,p1:H4,p0:H5,c_observed:CT\nc_public:S4,p0:C6,p1:CK,c_unobserved:\nc_public:D9,p1:C8,p0:C5,c_unobserved:\nc_public:HK,p1:CJ,p0:C7,c_unobserved:\nc_public:S6,p1:HK,p0:H9,c_unobserved:\nc_public:D2,p1:SK,p0:S9,c_unobserved:\n" +ObservationString(0) = "p0,CA,CQ,DK,DQ,DJ,D8,D3,HA,H6,H2,SA,S7,S2,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK,1:CJ,0:C7,-1:S6,1:HK,0:H9,-1:D2,1:SK,0:S9," +ObservationString(1) = "p1,CT,C9,C2,D9,D7,D6,D2,HT,H8,SJ,S6,S4,S3,-1:S8,0:H3,1:HQ,-1:C2,1:C4,0:C3,-1:DJ,1:H7,0:HJ,-1:H8,0:S5,1:S8,-1:C6,1:ST,0:SQ,-1:D6,0:D5,1:DA,-1:C8,1:DT,0:D4,-1:C5,1:H4,0:H5,-1:S4,0:C6,1:CK,-1:D9,1:C8,0:C5,-1:HK,1:CJ,0:C7,-1:S6,1:HK,0:H9,-1:D2,1:SK,0:S9," +Rewards() = [13, -13] +Returns() = [13, -13] diff --git a/open_spiel/python/tests/pyspiel_test.py b/open_spiel/python/tests/pyspiel_test.py index b4524936f1..f54c5133fb 100644 --- a/open_spiel/python/tests/pyspiel_test.py +++ b/open_spiel/python/tests/pyspiel_test.py @@ -57,6 +57,7 @@ "efg_game", "euchre", "first_sealed_auction", + "german_whist_foregame", "gin_rummy", "go", "goofspiel",