Skip to content

Commit

Permalink
Merge pull request #868 from acforvs:feature/nim
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 459879018
Change-Id: I0ccfe747dae16c8d29367dd33bcdd538957f15f2
  • Loading branch information
lanctot committed Jul 11, 2022
2 parents eab604e + 688b76d commit 0c871e1
Show file tree
Hide file tree
Showing 7 changed files with 709 additions and 0 deletions.
12 changes: 12 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,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.
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
233 changes: 233 additions & 0 deletions open_spiel/games/nim.cc
Original file line number Diff line number Diff line change
@@ -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 <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();
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<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,
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<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::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;

// 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<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
119 changes: 119 additions & 0 deletions open_spiel/games/nim.h
Original file line number Diff line number Diff line change
@@ -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 <array>
#include <map>
#include <memory>
#include <string>
#include <vector>

#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<const Game> game, int num_piles,
std::vector<int> 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<double> Returns() const override;
std::string InformationStateString(Player player) const override;
std::string ObservationString(Player player) const override;
void ObservationTensor(Player player,
absl::Span<float> values) const override;
std::unique_ptr<State> Clone() const override;
void UndoAction(Player player, Action move) override;
std::vector<Action> LegalActions() const override;
Player outcome() const { return outcome_; }

protected:
void DoApplyAction(Action move) override;
int num_piles_ = kDefaultNumPiles;
std::vector<int> piles_;

private:
bool IsEmpty() const;
std::pair<int, int> 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 &params);
int NumDistinctActions() const override;
std::unique_ptr<State> NewInitialState() const override {
return std::unique_ptr<State>(
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<int> 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<int> 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_
Loading

0 comments on commit 0c871e1

Please sign in to comment.