diff --git a/docs/games.md b/docs/games.md index 79c94e41cc..c1490c3313 100644 --- a/docs/games.md +++ b/docs/games.md @@ -53,6 +53,7 @@ Status | Game ![](_static/green_circ10.png "green circle") | [Mean Field Game : routing](#mean-field-game--routing) ~ | [Morpion Solitaire (4D)](#morpion-solitaire-4d) ![](_static/green_circ10.png "green circle") | [Negotiation](#negotiation) +~ | [Nim](#nim) X | [Oh Hell](#oh-hell) ![](_static/green_circ10.png "green circle") | [Oshi-Zumo](#oshi-zumo) ![](_static/green_circ10.png "green circle") | [Oware](#oware) @@ -521,6 +522,17 @@ Status | Game * [Lewis et al. '17](https://arxiv.org/abs/1706.05125), [Cao et al. '18](https://arxiv.org/abs/1804.03980) +### Nim + +* Two agents take objects from distinct piles trying to either avoid taking + the last one or take it. Any positive number of objects can be taken on each + turn given they all come from the same pile. +* Traditional mathematical game. +* Deterministic. +* Perfect information. +* 2 players. +* [Wikipedia](https://en.wikipedia.org/wiki/Nim) + ### Oh Hell * A card game where players try to win exactly a declared number of tricks. diff --git a/open_spiel/games/CMakeLists.txt b/open_spiel/games/CMakeLists.txt index 624c662fb3..814fbb46d2 100644 --- a/open_spiel/games/CMakeLists.txt +++ b/open_spiel/games/CMakeLists.txt @@ -113,6 +113,8 @@ set(GAME_SOURCES negotiation.h nfg_game.cc nfg_game.h + nim.cc + nim.h oh_hell.cc oh_hell.h oshi_zumo.cc @@ -462,6 +464,10 @@ add_executable(nfg_game_test nfg_game_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(nfg_game_test nfg_game_test) +add_executable(nim_test nim_test.cc ${OPEN_SPIEL_OBJECTS} + $) +add_test(nim_test nim_test) + add_executable(oh_hell_test oh_hell_test.cc ${OPEN_SPIEL_OBJECTS} $) add_test(oh_hell_test oh_hell_test) diff --git a/open_spiel/games/nim.cc b/open_spiel/games/nim.cc new file mode 100644 index 0000000000..7c2bc5ec8d --- /dev/null +++ b/open_spiel/games/nim.cc @@ -0,0 +1,233 @@ +// 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. + +#include "open_spiel/games/nim.h" + +#include +#include +#include +#include +#include + +#include "open_spiel/abseil-cpp/absl/strings/numbers.h" +#include "open_spiel/spiel_utils.h" + +namespace open_spiel { +namespace nim { +namespace { + +constexpr char kDefaultPileSizes[] = "1;3;5;7"; + +std::vector ParsePilesString(const std::string &str) { + std::vector sizes = absl::StrSplit(str, ';'); + std::vector pile_sizes; + for (const auto &sz : sizes) { + int val; + if (!absl::SimpleAtoi(sz, &val)) { + SpielFatalError(absl::StrCat("Could not parse size '", sz, + "' of pile_sizes string '", str, + "' as an integer")); + } + pile_sizes.push_back(val); + } + return pile_sizes; +} + +// Facts about the game. +const GameType kGameType{ + /*short_name=*/"nim", + /*long_name=*/"Nim", + GameType::Dynamics::kSequential, + GameType::ChanceMode::kDeterministic, + GameType::Information::kPerfectInformation, + 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=*/true, + { + {"pile_sizes", GameParameter(std::string(kDefaultPileSizes))}, + {"is_misere", GameParameter(kDefaultIsMisere)}, + }}; + +std::shared_ptr Factory(const GameParameters ¶ms) { + return std::shared_ptr(new NimGame(params)); +} + +REGISTER_SPIEL_GAME(kGameType, Factory); + +} // namespace + +NimGame::NimGame(const GameParameters ¶ms) + : Game(kGameType, params), + piles_(ParsePilesString(ParameterValue("pile_sizes"))), + is_misere_(ParameterValue("is_misere")) { + num_piles_ = piles_.size(); + max_num_per_pile_ = *std::max_element(piles_.begin(), piles_.end()); +} + +int NimGame::NumDistinctActions() const { + if (piles_.empty()) { + return 0; + } + // action_id = (take - 1) * num_piles_ + pile_idx < (max_take - 1) * + // num_piles_ + num_piles = max_take * num_piles_ + return num_piles_ * max_num_per_pile_ + 1; +} + +int NimGame::MaxGameLength() const { + // players can take only 1 object at every step + return std::accumulate(piles_.begin(), piles_.end(), 0); +} + +std::pair NimState::UnpackAction(Action action_id) const { + // action_id = (take - 1) * num_piles_ + pile_idx + int pile_idx = action_id % num_piles_; + int take = (action_id - pile_idx) / num_piles_ + 1; + return {pile_idx, take}; +} + +bool NimState::IsEmpty() const { + return std::accumulate(piles_.begin(), piles_.end(), 0) == 0; +} + +void NimState::DoApplyAction(Action move) { + SPIEL_CHECK_FALSE(IsTerminal()); + std::pair action = UnpackAction(move); + int pile_idx = action.first, take = action.second; + + SPIEL_CHECK_LT(pile_idx, piles_.size()); + SPIEL_CHECK_GT(take, 0); + SPIEL_CHECK_LE(take, piles_[pile_idx]); + + piles_[pile_idx] -= take; + if (IsEmpty()) { + outcome_ = is_misere_ ? 1 - current_player_ : current_player_; + } + current_player_ = 1 - current_player_; + num_moves_ += 1; +} + +std::vector NimState::LegalActions() const { + if (IsTerminal()) return {}; + std::vector moves; + for (std::size_t pile_idx = 0; pile_idx < piles_.size(); pile_idx++) { + // the player has to take at least one object from a pile + for (int take = 1; take <= piles_[pile_idx]; take++) { + moves.push_back((take - 1) * num_piles_ + (int)pile_idx); + } + } + std::sort(moves.begin(), moves.end()); + return moves; +} + +std::string NimState::ActionToString(Player player, Action action_id) const { + std::pair action = UnpackAction(action_id); + int pile_idx = action.first, take = action.second; + return absl::StrCat("pile:", pile_idx + 1, ", take:", take, ";"); +} + +NimState::NimState(std::shared_ptr game, int num_piles, + std::vector piles, bool is_misere, + int max_num_per_pile) + : State(game), + num_piles_(num_piles), + piles_(piles), + is_misere_(is_misere), + max_num_per_pile_(max_num_per_pile) {} + +std::string NimState::ToString() const { + std::string str; + absl::StrAppend(&str, "(", current_player_, "): "); + for (std::size_t pile_idx = 0; pile_idx < piles_.size(); pile_idx++) { + absl::StrAppend(&str, piles_[pile_idx]); + if (pile_idx != piles_.size() - 1) { + absl::StrAppend(&str, " "); + } + } + return str; +} + +bool NimState::IsTerminal() const { + return outcome_ != kInvalidPlayer || IsEmpty(); +} + +std::vector NimState::Returns() const { + if (outcome_ == Player{0}) { + return {1.0, -1.0}; + } else if (outcome_ == Player{1}) { + return {-1.0, 1.0}; + } else { + return {0.0, 0.0}; + } +} + +std::string NimState::InformationStateString(Player player) const { + SPIEL_CHECK_GE(player, 0); + SPIEL_CHECK_LT(player, num_players_); + return HistoryString(); +} + +std::string NimState::ObservationString(Player player) const { + SPIEL_CHECK_GE(player, 0); + SPIEL_CHECK_LT(player, num_players_); + return ToString(); +} + +void NimState::ObservationTensor(Player player, + absl::Span values) const { + // [one-hot player] + [IsTerminal()] + [binary representation of num_piles] + + // [binary representation of every pile] + SPIEL_CHECK_GE(player, 0); + SPIEL_CHECK_LT(player, num_players_); + std::fill(values.begin(), values.end(), 0); + + int offset = 0; + values[current_player_] = 1; + offset += 2; + values[offset] = IsTerminal() ? 1 : 0; + offset += 1; + + // num_piles (which is >= 1) + values[offset + num_piles_ - 1] = 1; + offset += num_piles_; + + for (std::size_t pile_idx = 0; pile_idx < piles_.size(); pile_idx++) { + values[offset + piles_[pile_idx]] = 1; + offset += max_num_per_pile_ + 1; + } + + SPIEL_CHECK_EQ(offset, values.size()); +} + +void NimState::UndoAction(Player player, Action move) { + std::pair action = UnpackAction(move); + int pile_idx = action.first, take = action.second; + piles_[pile_idx] += take; + current_player_ = player; + outcome_ = kInvalidPlayer; + num_moves_ -= 1; + history_.pop_back(); + --move_number_; +} + +std::unique_ptr NimState::Clone() const { + return std::unique_ptr(new NimState(*this)); +} + +} // namespace nim +} // namespace open_spiel diff --git a/open_spiel/games/nim.h b/open_spiel/games/nim.h new file mode 100644 index 0000000000..73d0bf6477 --- /dev/null +++ b/open_spiel/games/nim.h @@ -0,0 +1,119 @@ +// 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. + +#ifndef OPEN_SPIEL_GAMES_NIM_H_ +#define OPEN_SPIEL_GAMES_NIM_H_ + +#include +#include +#include +#include +#include + +#include "open_spiel/spiel.h" + +// Nim: +// * Two players take turns removing objects from distinct piles; +// * On each turn, a player must remove at least one object, +// and may remove any number of objects provided they all come from the +// same heap or pile; +// * Depending on the version, the goal of the game is either to avoid taking +// the last object or to take it. Please see https://en.wikipedia.org/wiki/Nim +// for more + +namespace open_spiel { +namespace nim { + +// Constants. +inline constexpr int kNumPlayers = 2; +inline constexpr int kDefaultNumPiles = 3; +inline constexpr bool kDefaultIsMisere = true; + +// State of an in-play game. +class NimState : public State { + public: + explicit NimState(std::shared_ptr game, int num_piles, + std::vector piles, bool is_misere, + int max_num_per_pile); + + NimState(const NimState &) = default; + NimState &operator=(const NimState &) = default; + + Player CurrentPlayer() const override { + return IsTerminal() ? kTerminalPlayerId : current_player_; + } + std::string ActionToString(Player player, Action action_id) const override; + std::string ToString() const override; + bool IsTerminal() const override; + std::vector Returns() const override; + std::string InformationStateString(Player player) const override; + std::string ObservationString(Player player) const override; + void ObservationTensor(Player player, + absl::Span values) const override; + std::unique_ptr Clone() const override; + void UndoAction(Player player, Action move) override; + std::vector LegalActions() const override; + Player outcome() const { return outcome_; } + + protected: + void DoApplyAction(Action move) override; + int num_piles_ = kDefaultNumPiles; + std::vector piles_; + + private: + bool IsEmpty() const; + std::pair UnpackAction(Action action_id) const; + Player current_player_ = 0; // Player zero goes first + Player outcome_ = kInvalidPlayer; + int num_moves_ = 0; + bool is_misere_ = kDefaultIsMisere; + const int max_num_per_pile_; +}; + +// Game object. +class NimGame : public Game { + public: + explicit NimGame(const GameParameters ¶ms); + int NumDistinctActions() const override; + std::unique_ptr NewInitialState() const override { + return std::unique_ptr( + new NimState(shared_from_this(), num_piles_, piles_, is_misere_, + max_num_per_pile_)); + } + int NumPlayers() const override { return kNumPlayers; } + double MinUtility() const override { return -1; } + double UtilitySum() const override { return 0; } + double MaxUtility() const override { return 1; } + std::vector ObservationTensorShape() const override { + return { + 2 + // Turn + 1 + // Is terminal? + num_piles_ + // One-hot bit for the number `num_piles_` + // One hot representation of the quantity in each pile. + num_piles_ * (max_num_per_pile_ + 1) + }; + }; + int MaxGameLength() const override; + + private: + std::vector piles_; + int num_piles_ = kDefaultNumPiles; + bool is_misere_ = kDefaultIsMisere; + int max_num_per_pile_; +}; + +} // namespace nim +} // namespace open_spiel + +#endif // OPEN_SPIEL_GAMES_NIM_H_ diff --git a/open_spiel/games/nim_test.cc b/open_spiel/games/nim_test.cc new file mode 100644 index 0000000000..3e54831583 --- /dev/null +++ b/open_spiel/games/nim_test.cc @@ -0,0 +1,158 @@ +// 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. + +#include "open_spiel/algorithms/value_iteration.h" +#include "open_spiel/spiel.h" +#include "open_spiel/tests/basic_tests.h" + +namespace open_spiel { +namespace nim { +namespace { + +namespace testing = open_spiel::testing; +namespace algorithms = open_spiel::algorithms; + +void BasicNimTests() { + testing::LoadGameTest("nim"); + testing::RandomSimTest(*LoadGame("nim"), 100); + testing::RandomSimTestWithUndo(*LoadGame("nim"), 10); + testing::RandomSimTest( + *LoadGame("nim", + { + {"pile_sizes", GameParameter("100;200;300")}, + }), + 10); + testing::RandomSimTest( + *LoadGame("nim", + { + {"pile_sizes", + GameParameter("10000;2000;3000;12414;1515;53252;1;35126")}, + }), + 10); + testing::RandomSimTest( + *LoadGame("nim", + { + {"pile_sizes", GameParameter("1;2;3;4;5;6;7;8;9;10")}, + {"is_misere", GameParameter(false)}, + }), + 10); +} + +void SinglePileNormalTest() { + std::shared_ptr game = + LoadGame("nim", { + {"pile_sizes", GameParameter("100")}, + {"is_misere", GameParameter(false)}, + }); + std::unique_ptr state = game->NewInitialState(); + std::vector actions = state->LegalActions(); + SPIEL_CHECK_EQ(actions.size(), 100); + + state->ApplyAction(actions.back()); + SPIEL_CHECK_EQ(state->IsTerminal(), 1); + SPIEL_CHECK_EQ(state->PlayerReturn(0), 1); + SPIEL_CHECK_EQ(state->PlayerReturn(1), -1); +} + +void SinglePileMisereTest() { + std::shared_ptr game = + LoadGame("nim", { + {"pile_sizes", GameParameter("100")}, + }); + std::unique_ptr state = game->NewInitialState(); + std::vector actions = state->LegalActions(); + SPIEL_CHECK_EQ(actions.size(), 100); + + state->ApplyAction(actions.back()); + SPIEL_CHECK_EQ(state->IsTerminal(), 1); + SPIEL_CHECK_EQ(state->PlayerReturn(0), -1); + SPIEL_CHECK_EQ(state->PlayerReturn(1), 1); +} + +void VISinglePileNormalTest() { + std::shared_ptr game = + LoadGame("nim", { + {"pile_sizes", GameParameter("100")}, + {"is_misere", GameParameter(false)}, + }); + auto values = algorithms::ValueIteration(*game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 100"], 1); +} + +void VISinglePileMisereTest() { + std::shared_ptr game = + LoadGame("nim", { + {"pile_sizes", GameParameter("100")}, + }); + auto values = algorithms::ValueIteration(*game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 100"], 1); +} + +// See "Winning positions" here +// https://en.wikipedia.org/wiki/Nim +// to understand the "pile_sizes" parameter from the tests below +void VIThreeOnesNormalTest() { + std::shared_ptr normal_game = + LoadGame("nim", { + {"pile_sizes", GameParameter("1;1;1")}, + {"is_misere", GameParameter(false)}, + }); + auto values = algorithms::ValueIteration(*normal_game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 1 1 1"], 1); +} + +void VIThreeOnesMisereTest() { + std::shared_ptr game = + LoadGame("nim", { + {"pile_sizes", GameParameter("1;1;1")}, + }); + auto values = algorithms::ValueIteration(*game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 1 1 1"], -1); +} + +void VIThreePilesTest() { + std::shared_ptr normal_game = + LoadGame("nim", { + {"pile_sizes", GameParameter("5;8;13")}, + {"is_misere", GameParameter(false)}, + }); + auto values = algorithms::ValueIteration(*normal_game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 5 8 13"], -1); +} + +void VIFourPilesTest() { + std::shared_ptr normal_game = + LoadGame("nim", { + {"pile_sizes", GameParameter("2;3;8;10")}, + {"is_misere", GameParameter(false)}, + }); + auto values = algorithms::ValueIteration(*normal_game, -1, 0.01); + SPIEL_CHECK_EQ(values["(0): 2 3 8 10"], 1); +} + +} // namespace +} // namespace nim +} // namespace open_spiel + +int main(int argc, char **argv) { + open_spiel::nim::BasicNimTests(); + open_spiel::nim::SinglePileNormalTest(); + open_spiel::nim::SinglePileMisereTest(); + open_spiel::nim::VISinglePileNormalTest(); + open_spiel::nim::VISinglePileMisereTest(); + open_spiel::nim::VIThreeOnesNormalTest(); + open_spiel::nim::VIThreeOnesMisereTest(); + open_spiel::nim::VIThreePilesTest(); + open_spiel::nim::VIFourPilesTest(); +} diff --git a/open_spiel/integration_tests/playthroughs/nim.txt b/open_spiel/integration_tests/playthroughs/nim.txt new file mode 100644 index 0000000000..1d94516f23 --- /dev/null +++ b/open_spiel/integration_tests/playthroughs/nim.txt @@ -0,0 +1,180 @@ +game: nim + +GameType.chance_mode = ChanceMode.DETERMINISTIC +GameType.dynamics = Dynamics.SEQUENTIAL +GameType.information = Information.PERFECT_INFORMATION +GameType.long_name = "Nim" +GameType.max_num_players = 2 +GameType.min_num_players = 2 +GameType.parameter_specification = ["is_misere", "pile_sizes"] +GameType.provides_information_state_string = True +GameType.provides_information_state_tensor = False +GameType.provides_observation_string = True +GameType.provides_observation_tensor = True +GameType.provides_factored_observation_string = False +GameType.reward_model = RewardModel.TERMINAL +GameType.short_name = "nim" +GameType.utility = Utility.ZERO_SUM + +NumDistinctActions() = 29 +PolicyTensorShape() = [29] +MaxChanceOutcomes() = 0 +GetParameters() = {is_misere=True,pile_sizes=1;3;5;7} +NumPlayers() = 2 +MinUtility() = -1.0 +MaxUtility() = 1.0 +UtilitySum() = 0.0 +ObservationTensorShape() = [39] +ObservationTensorLayout() = TensorLayout.CHW +ObservationTensorSize() = 39 +MaxGameLength() = 16 +ToString() = "nim()" + +# State 0 +# (0): 1 3 5 7 +IsTerminal() = False +History() = [] +HistoryString() = "" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "" +InformationStateString(1) = "" +ObservationString(0) = "(0): 1 3 5 7" +ObservationString(1) = "(0): 1 3 5 7" +ObservationTensor(0): ◉◯◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉ +ObservationTensor(1): ◉◯◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [0, 1, 2, 3, 5, 6, 7, 9, 10, 11, 14, 15, 18, 19, 23, 27] +StringLegalActions() = ["pile:1, take:1;", "pile:2, take:1;", "pile:3, take:1;", "pile:4, take:1;", "pile:2, take:2;", "pile:3, take:2;", "pile:4, take:2;", "pile:2, take:3;", "pile:3, take:3;", "pile:4, take:3;", "pile:3, take:4;", "pile:4, take:4;", "pile:3, take:5;", "pile:4, take:5;", "pile:4, take:6;", "pile:4, take:7;"] + +# Apply action "pile:4, take:5;" +action: 19 + +# State 1 +# (1): 1 3 5 2 +IsTerminal() = False +History() = [19] +HistoryString() = "19" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "19" +InformationStateString(1) = "19" +ObservationString(0) = "(1): 1 3 5 2" +ObservationString(1) = "(1): 1 3 5 2" +ObservationTensor(0): ◯◉◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯ +ObservationTensor(1): ◯◉◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [0, 1, 2, 3, 5, 6, 7, 9, 10, 14, 18] +StringLegalActions() = ["pile:1, take:1;", "pile:2, take:1;", "pile:3, take:1;", "pile:4, take:1;", "pile:2, take:2;", "pile:3, take:2;", "pile:4, take:2;", "pile:2, take:3;", "pile:3, take:3;", "pile:3, take:4;", "pile:3, take:5;"] + +# Apply action "pile:3, take:5;" +action: 18 + +# State 2 +# (0): 1 3 0 2 +IsTerminal() = False +History() = [19, 18] +HistoryString() = "19, 18" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "19, 18" +InformationStateString(1) = "19, 18" +ObservationString(0) = "(0): 1 3 0 2" +ObservationString(1) = "(0): 1 3 0 2" +ObservationTensor(0): ◉◯◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +ObservationTensor(1): ◉◯◯◯◯◯◉◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [0, 1, 3, 5, 7, 9] +StringLegalActions() = ["pile:1, take:1;", "pile:2, take:1;", "pile:4, take:1;", "pile:2, take:2;", "pile:4, take:2;", "pile:2, take:3;"] + +# Apply action "pile:1, take:1;" +action: 0 + +# State 3 +# (1): 0 3 0 2 +IsTerminal() = False +History() = [19, 18, 0] +HistoryString() = "19, 18, 0" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "19, 18, 0" +InformationStateString(1) = "19, 18, 0" +ObservationString(0) = "(1): 0 3 0 2" +ObservationString(1) = "(1): 0 3 0 2" +ObservationTensor(0): ◯◉◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +ObservationTensor(1): ◯◉◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [1, 3, 5, 7, 9] +StringLegalActions() = ["pile:2, take:1;", "pile:4, take:1;", "pile:2, take:2;", "pile:4, take:2;", "pile:2, take:3;"] + +# Apply action "pile:2, take:1;" +action: 1 + +# State 4 +# (0): 0 2 0 2 +IsTerminal() = False +History() = [19, 18, 0, 1] +HistoryString() = "19, 18, 0, 1" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 0 +InformationStateString(0) = "19, 18, 0, 1" +InformationStateString(1) = "19, 18, 0, 1" +ObservationString(0) = "(0): 0 2 0 2" +ObservationString(1) = "(0): 0 2 0 2" +ObservationTensor(0): ◉◯◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +ObservationTensor(1): ◉◯◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [1, 3, 5, 7] +StringLegalActions() = ["pile:2, take:1;", "pile:4, take:1;", "pile:2, take:2;", "pile:4, take:2;"] + +# Apply action "pile:4, take:2;" +action: 7 + +# State 5 +# (1): 0 2 0 0 +IsTerminal() = False +History() = [19, 18, 0, 1, 7] +HistoryString() = "19, 18, 0, 1, 7" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = 1 +InformationStateString(0) = "19, 18, 0, 1, 7" +InformationStateString(1) = "19, 18, 0, 1, 7" +ObservationString(0) = "(1): 0 2 0 0" +ObservationString(1) = "(1): 0 2 0 0" +ObservationTensor(0): ◯◉◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯ +ObservationTensor(1): ◯◉◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯ +Rewards() = [0, 0] +Returns() = [0, 0] +LegalActions() = [1, 5] +StringLegalActions() = ["pile:2, take:1;", "pile:2, take:2;"] + +# Apply action "pile:2, take:2;" +action: 5 + +# State 6 +# (0): 0 0 0 0 +IsTerminal() = True +History() = [19, 18, 0, 1, 7, 5] +HistoryString() = "19, 18, 0, 1, 7, 5" +IsChanceNode() = False +IsSimultaneousNode() = False +CurrentPlayer() = -4 +InformationStateString(0) = "19, 18, 0, 1, 7, 5" +InformationStateString(1) = "19, 18, 0, 1, 7, 5" +ObservationString(0) = "(0): 0 0 0 0" +ObservationString(1) = "(0): 0 0 0 0" +ObservationTensor(0): ◉◯◉◯◯◯◉◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯ +ObservationTensor(1): ◉◯◉◯◯◯◉◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯ +Rewards() = [1, -1] +Returns() = [1, -1] diff --git a/open_spiel/python/tests/pyspiel_test.py b/open_spiel/python/tests/pyspiel_test.py index 8c571a4f70..f2aabd7e0c 100644 --- a/open_spiel/python/tests/pyspiel_test.py +++ b/open_spiel/python/tests/pyspiel_test.py @@ -83,6 +83,7 @@ "morpion_solitaire", "negotiation", "nfg_game", + "nim", "normal_form_extensive_game", "oh_hell", "oshi_zumo",