diff --git a/docs/games.md b/docs/games.md
index 79c94e41cc..5ae7594e88 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,16 @@ 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/CMakeLists.txt b/open_spiel/CMakeLists.txt
index 0c185185de..e8ee9c4856 100644
--- a/open_spiel/CMakeLists.txt
+++ b/open_spiel/CMakeLists.txt
@@ -197,6 +197,7 @@ set (OPEN_SPIEL_CORE_FILES
# We add the subdirectory here so open_spiel_core can #include absl.
set(ABSL_PROPAGATE_CXX_STD ON)
add_subdirectory (abseil-cpp)
+include_directories (abseil-cpp)
# Just the core without any of the games
add_library(open_spiel_core OBJECT ${OPEN_SPIEL_CORE_FILES})
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..8093ceaac7
--- /dev/null
+++ b/open_spiel/games/nim.cc
@@ -0,0 +1,234 @@
+// Copyright 2022 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();
+}
+
+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_
+ int max_take = *std::max_element(piles_.begin(), piles_.end());
+ return num_piles_ * max_take + 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)
+ : State(game), num_piles_(num_piles),
+ piles_(piles), is_misere_(is_misere) {}
+
+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::WriteIntToObservation(absl::Span &values,
+ int &offset,
+ int num) const {
+ for (int i = kBits - 1; i >= 0; i--) {
+ values[offset + (kBits - i - 1)] = (num >> i) & 1U;
+ }
+ offset += kBits;
+}
+
+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;
+
+ WriteIntToObservation(values, offset, num_piles_);
+ for (std::size_t pile_idx = 0; pile_idx < piles_.size(); pile_idx++) {
+ WriteIntToObservation(values, offset, piles_[pile_idx]);
+ }
+}
+
+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..f10da21cbe
--- /dev/null
+++ b/open_spiel/games/nim.h
@@ -0,0 +1,124 @@
+// Copyright 2022 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