Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new game: Nim #868

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/games.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Status | Game
![](_static/green_circ10.png "green circle") | [Mean Field Game : routing](#mean-field-game--routing)
<font color="orange"><b>~</b></font> | [Morpion Solitaire (4D)](#morpion-solitaire-4d)
![](_static/green_circ10.png "green circle") | [Negotiation](#negotiation)
<font color="orange"><b>~</b></font> | [Nim](#nim)
<font color="orange"><b>X</b></font> | [Oh Hell](#oh-hell)
![](_static/green_circ10.png "green circle") | [Oshi-Zumo](#oshi-zumo)
![](_static/green_circ10.png "green circle") | [Oware](#oware)
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions open_spiel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
6 changes: 6 additions & 0 deletions open_spiel/games/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -462,6 +464,10 @@ add_executable(nfg_game_test nfg_game_test.cc ${OPEN_SPIEL_OBJECTS}
$<TARGET_OBJECTS:algorithms>)
add_test(nfg_game_test nfg_game_test)

add_executable(nim_test nim_test.cc ${OPEN_SPIEL_OBJECTS}
$<TARGET_OBJECTS:tests>)
add_test(nim_test nim_test)

add_executable(oh_hell_test oh_hell_test.cc ${OPEN_SPIEL_OBJECTS}
$<TARGET_OBJECTS:tests>)
add_test(oh_hell_test oh_hell_test)
Expand Down
234 changes: 234 additions & 0 deletions open_spiel/games/nim.cc
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#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<int> ParsePilesString(const std::string &str) {
std::vector<std::string> sizes = absl::StrSplit(str, ';');
std::vector<int> 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<const Game> Factory(const GameParameters &params) {
return std::shared_ptr<const Game>(new NimGame(params));
}

REGISTER_SPIEL_GAME(kGameType, Factory);

} // namespace

NimGame::NimGame(const GameParameters &params)
: Game(kGameType, params),
piles_(ParsePilesString(ParameterValue<std::string>("pile_sizes"))),
is_misere_(ParameterValue<bool>("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<int, int> 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<int, int> 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<Action> NimState::LegalActions() const {
if (IsTerminal()) return {};
std::vector<Action> 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<int, int> 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<const Game> game, int num_piles,
std::vector<int> 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<double> 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<float> &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<float> 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<int, int> 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<State> NimState::Clone() const {
return std::unique_ptr<State>(new NimState(*this));
}

} // namespace nim
} // namespace open_spiel
Loading