Repository: google-deepmind/open_spiel (language: python)

Issue:
Requested Observer type not available for kNone and kSinglePlayer
I have a local build that I'm testing out and I'm getting errors for single player games.

This is the error I get when trying to run the python package via rl_envionment.Environment('blackjack'):

```
[ValueError: observation_tensor not supported by blackjack]()
```

I also found that I get these errors during playthroughs when I'm running build_and_run_tests.sh

```
OpenSpiel exception: Requested Observer type not available: IIGObservationType{perfect_recall=false, public_info=true, private_info=kNone}
OpenSpiel exception: Requested Observer type not available: IIGObservationType{perfect_recall=false, public_info=false, private_info=kSinglePlayer}
```



Discussion / hints:
Hi @kimyoonduk, 

Yes, looking at the blackjack implementation (see [here](https://github.com/deepmind/open_spiel/blob/39abaf14df4d5e4906806375dfdfa26f8572b916/open_spiel/games/blackjack.cc#L53)), it seems to not have any observation or information state tensors defined, which means you (currently) can't use it for RL experiments with neural networks. This sometimes happens when we get contributions of games that are not complete, but e.g. it could still be used for some tabular methods. 

I think in the case of blackjack, it should be fairly easy to add a simple observation tensor and I'd be happy to help describe how to do that if you're interested. Thanks for bringing it to our attention because if you don't add it now we'll probably do it eventually.

Edit: you can ignore those errors, they are not an indication of a problem and we will probably suppress them soon.


Fix patch:
diff --git a/open_spiel/games/blackjack.cc b/open_spiel/games/blackjack.cc
index 5f74a4d823..813389eeab 100644
--- a/open_spiel/games/blackjack.cc
+++ b/open_spiel/games/blackjack.cc
@@ -50,7 +50,7 @@ const GameType kGameType{/*short_name=*/"blackjack",
                          /*provides_information_state_string=*/false,
                          /*provides_information_state_tensor=*/false,
                          /*provides_observation_string=*/true,
-                         /*provides_observation_tensor=*/false,
+                         /*provides_observation_tensor=*/true,
                          /*parameter_specification=*/{}};
 
 static std::shared_ptr<const Game> Factory(const GameParameters& params) {
@@ -107,6 +107,38 @@ std::string BlackjackState::ObservationString(Player player) const {
   return ToString();
 }
 
+void BlackjackState::WriteAcesToObservation(absl::Span<float> &values,
+                                            int &offset,
+                                            int num) const {
+  // bits to represent an amount of aces
+  int kNumBitsForAces = __builtin_clz(1) - __builtin_clz(kNumSuits) + 1;
+  for (int i = kNumBitsForAces - 1; i >= 0; i--) {
+    values[offset + kNumBitsForAces - i - 1] = (num >> i) & 1U;
+  }
+  offset += kNumBitsForAces;
+}
+
+void BlackjackState::ObservationTensor(Player player,
+                                       absl::Span<float> values) const {
+  std::fill(values.begin(), values.end(), 0);
+  int offset = 0;
+  if (cur_player_ + 1 >= 0) {  // do not support kTerminalPlayerId
+    values[cur_player_ + 1] = 1;  // to support kChancePlayerId (equals to -1)
+  }
+  offset += game_->NumPlayers() + 1;
+
+  values[offset] = IsTerminal();
+  offset += 1;
+
+  for (std::size_t player_id = 0; player_id < cards_.size(); player_id++) {
+    WriteAcesToObservation(values, offset, num_aces_[player_id]);
+    for (const int &card: cards_[player_id]) {
+      values[offset + card] = 1;
+    }
+    offset += kDeckSize;
+  }
+}
+
 bool BlackjackState::InitialCardsDealt(int player) const {
   return cards_[player].size() >= kInitialCardsPerPlayer;
 }
diff --git a/open_spiel/games/blackjack.h b/open_spiel/games/blackjack.h
index 777d3e6c63..2baa433755 100644
--- a/open_spiel/games/blackjack.h
+++ b/open_spiel/games/blackjack.h
@@ -45,6 +45,8 @@ class BlackjackState : public State {
   bool IsTerminal() const override;
   std::vector<double> Returns() const override;
   std::string ObservationString(Player player) const override;
+  void ObservationTensor(Player player,
+                         absl::Span<float> values) const override;
   ActionsAndProbs ChanceOutcomes() const override;
 
   std::unique_ptr<State> Clone() const override;
@@ -64,6 +66,9 @@ class BlackjackState : public State {
 
  private:
   void MaybeApplyDealerAction();
+  void WriteAcesToObservation(absl::Span<float> &values,
+                              int &offset,
+                              int num) const;
 
   // Initialize to bad/invalid values. Use open_spiel::NewInitialState()
 
@@ -94,6 +99,16 @@ class BlackjackGame : public Game {
   int NumPlayers() const override { return 1; }
   double MinUtility() const override { return -1; }
   double MaxUtility() const override { return +1; }
+  std::vector<int> ObservationTensorShape() const override {
+    // bits to represent an amount of aces
+    int kNumBitsForAces = __builtin_clz(1) - __builtin_clz(kNumSuits) + 1;
+    return {
+        NumPlayers() + 1 +  // turn
+        1 + // is terminal?
+        kNumBitsForAces * (NumPlayers() + 1) + // num_aces_ for every player
+        kDeckSize * (NumPlayers() + 1) // one-hot of the deck for every player
+    };
+  };
 };
 
 }  // namespace blackjack


Test patch:
diff --git a/open_spiel/integration_tests/playthroughs/blackjack.txt b/open_spiel/integration_tests/playthroughs/blackjack.txt
index ad2bfef042..8c1f827ca0 100644
--- a/open_spiel/integration_tests/playthroughs/blackjack.txt
+++ b/open_spiel/integration_tests/playthroughs/blackjack.txt
@@ -10,7 +10,7 @@ GameType.parameter_specification = []
 GameType.provides_information_state_string = False
 GameType.provides_information_state_tensor = False
 GameType.provides_observation_string = True
-GameType.provides_observation_tensor = False
+GameType.provides_observation_tensor = True
 GameType.provides_factored_observation_string = False
 GameType.reward_model = RewardModel.TERMINAL
 GameType.short_name = "blackjack"
@@ -24,6 +24,9 @@ NumPlayers() = 1
 MinUtility() = -1.0
 MaxUtility() = 1.0
 UtilitySum() = None
+ObservationTensorShape() = [113]
+ObservationTensorLayout() = TensorLayout.CHW
+ObservationTensorSize() = 113
 MaxGameLength() = 12
 ToString() = "blackjack()"
 
@@ -36,46 +39,49 @@ IsChanceNode() = True
 IsSimultaneousNode() = False
 CurrentPlayer() = -1
 ObservationString(0) = "Non-Ace Total: 0 0 Num Aces: 0 0, Chance Player\n"
+ObservationTensor(0): ◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
 ChanceOutcomes() = [(0, 0.019230769230769232), (1, 0.019230769230769232), (2, 0.019230769230769232), (3, 0.019230769230769232), (4, 0.019230769230769232), (5, 0.019230769230769232), (6, 0.019230769230769232), (7, 0.019230769230769232), (8, 0.019230769230769232), (9, 0.019230769230769232), (10, 0.019230769230769232), (11, 0.019230769230769232), (12, 0.019230769230769232), (13, 0.019230769230769232), (14, 0.019230769230769232), (15, 0.019230769230769232), (16, 0.019230769230769232), (17, 0.019230769230769232), (18, 0.019230769230769232), (19, 0.019230769230769232), (20, 0.019230769230769232), (21, 0.019230769230769232), (22, 0.019230769230769232), (23, 0.019230769230769232), (24, 0.019230769230769232), (25, 0.019230769230769232), (26, 0.019230769230769232), (27, 0.019230769230769232), (28, 0.019230769230769232), (29, 0.019230769230769232), (30, 0.019230769230769232), (31, 0.019230769230769232), (32, 0.019230769230769232), (33, 0.019230769230769232), (34, 0.019230769230769232), (35, 0.019230769230769232), (36, 0.019230769230769232), (37, 0.019230769230769232), (38, 0.019230769230769232), (39, 0.019230769230769232), (40, 0.019230769230769232), (41, 0.019230769230769232), (42, 0.019230769230769232), (43, 0.019230769230769232), (44, 0.019230769230769232), (45, 0.019230769230769232), (46, 0.019230769230769232), (47, 0.019230769230769232), (48, 0.019230769230769232), (49, 0.019230769230769232), (50, 0.019230769230769232), (51, 0.019230769230769232)]
 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", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CT", "CJ", "CQ", "CK", "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DT", "DJ", "DQ", "DK", "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "HT", "HJ", "HQ", "HK", "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "ST", "SJ", "SQ", "SK"]
 
-# Apply action "C2"
-action: 1
+# Apply action "S3"
+action: 41
 
 # State 1
-# Non-Ace Total: 2 0 Num Aces: 0 0, Chance Player
+# Non-Ace Total: 3 0 Num Aces: 0 0, Chance Player
 IsTerminal() = False
-History() = [1]
-HistoryString() = "1"
+History() = [41]
+HistoryString() = "41"
 IsChanceNode() = True
 IsSimultaneousNode() = False
 CurrentPlayer() = -1
-ObservationString(0) = "Non-Ace Total: 2 0 Num Aces: 0 0, Chance Player\n"
-ChanceOutcomes() = [(0, 0.0196078431372549), (2, 0.0196078431372549), (3, 0.0196078431372549), (4, 0.0196078431372549), (5, 0.0196078431372549), (6, 0.0196078431372549), (7, 0.0196078431372549), (8, 0.0196078431372549), (9, 0.0196078431372549), (10, 0.0196078431372549), (11, 0.0196078431372549), (12, 0.0196078431372549), (13, 0.0196078431372549), (14, 0.0196078431372549), (15, 0.0196078431372549), (16, 0.0196078431372549), (17, 0.0196078431372549), (18, 0.0196078431372549), (19, 0.0196078431372549), (20, 0.0196078431372549), (21, 0.0196078431372549), (22, 0.0196078431372549), (23, 0.0196078431372549), (24, 0.0196078431372549), (25, 0.0196078431372549), (26, 0.0196078431372549), (27, 0.0196078431372549), (28, 0.0196078431372549), (29, 0.0196078431372549), (30, 0.0196078431372549), (31, 0.0196078431372549), (32, 0.0196078431372549), (33, 0.0196078431372549), (34, 0.0196078431372549), (35, 0.0196078431372549), (36, 0.0196078431372549), (37, 0.0196078431372549), (38, 0.0196078431372549), (39, 0.0196078431372549), (40, 0.0196078431372549), (41, 0.0196078431372549), (42, 0.0196078431372549), (43, 0.0196078431372549), (44, 0.0196078431372549), (45, 0.0196078431372549), (46, 0.0196078431372549), (47, 0.0196078431372549), (48, 0.0196078431372549), (49, 0.0196078431372549), (50, 0.0196078431372549), (51, 0.0196078431372549)]
-LegalActions() = [0, 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", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CT", "CJ", "CQ", "CK", "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DT", "DJ", "DQ", "DK", "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "HT", "HJ", "HQ", "HK", "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "ST", "SJ", "SQ", "SK"]
+ObservationString(0) = "Non-Ace Total: 3 0 Num Aces: 0 0, Chance Player\n"
+ObservationTensor(0): ◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯
+ChanceOutcomes() = [(0, 0.0196078431372549), (1, 0.0196078431372549), (2, 0.0196078431372549), (3, 0.0196078431372549), (4, 0.0196078431372549), (5, 0.0196078431372549), (6, 0.0196078431372549), (7, 0.0196078431372549), (8, 0.0196078431372549), (9, 0.0196078431372549), (10, 0.0196078431372549), (11, 0.0196078431372549), (12, 0.0196078431372549), (13, 0.0196078431372549), (14, 0.0196078431372549), (15, 0.0196078431372549), (16, 0.0196078431372549), (17, 0.0196078431372549), (18, 0.0196078431372549), (19, 0.0196078431372549), (20, 0.0196078431372549), (21, 0.0196078431372549), (22, 0.0196078431372549), (23, 0.0196078431372549), (24, 0.0196078431372549), (25, 0.0196078431372549), (26, 0.0196078431372549), (27, 0.0196078431372549), (28, 0.0196078431372549), (29, 0.0196078431372549), (30, 0.0196078431372549), (31, 0.0196078431372549), (32, 0.0196078431372549), (33, 0.0196078431372549), (34, 0.0196078431372549), (35, 0.0196078431372549), (36, 0.0196078431372549), (37, 0.0196078431372549), (38, 0.0196078431372549), (39, 0.0196078431372549), (40, 0.0196078431372549), (42, 0.0196078431372549), (43, 0.0196078431372549), (44, 0.0196078431372549), (45, 0.0196078431372549), (46, 0.0196078431372549), (47, 0.0196078431372549), (48, 0.0196078431372549), (49, 0.0196078431372549), (50, 0.0196078431372549), (51, 0.0196078431372549)]
+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, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
+StringLegalActions() = ["CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CT", "CJ", "CQ", "CK", "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DT", "DJ", "DQ", "DK", "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "HT", "HJ", "HQ", "HK", "SA", "S2", "S4", "S5", "S6", "S7", "S8", "S9", "ST", "SJ", "SQ", "SK"]
 
-# Apply action "S7"
-action: 45
+# Apply action "CQ"
+action: 11
 
 # State 2
-# Apply action "D3"
-action: 15
+# Apply action "D2"
+action: 14
 
 # State 3
-# Apply action "HK"
-action: 38
+# Apply action "SK"
+action: 51
 
 # State 4
-# Non-Ace Total: 9 13 Num Aces: 0 0, Player's Turn
+# Non-Ace Total: 13 12 Num Aces: 0 0, Player's Turn
 IsTerminal() = False
-History() = [1, 45, 15, 38]
-HistoryString() = "1, 45, 15, 38"
+History() = [41, 11, 14, 51]
+HistoryString() = "41, 11, 14, 51"
 IsChanceNode() = False
 IsSimultaneousNode() = False
 CurrentPlayer() = 0
-ObservationString(0) = "Non-Ace Total: 9 13 Num Aces: 0 0, Player's Turn\n"
+ObservationString(0) = "Non-Ace Total: 13 12 Num Aces: 0 0, Player's Turn\n"
+ObservationTensor(0): ◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉
 Rewards() = [0]
 Returns() = [0]
 LegalActions() = [0, 1]
@@ -85,38 +91,40 @@ StringLegalActions() = ["Hit", "Stand"]
 action: 0
 
 # State 5
-# Apply action "SJ"
-action: 49
+# Apply action "S2"
+action: 40
 
 # State 6
-# Non-Ace Total: 19 13 Num Aces: 0 0, Player's Turn
+# Non-Ace Total: 15 12 Num Aces: 0 0, Player's Turn
 IsTerminal() = False
-History() = [1, 45, 15, 38, 0, 49]
-HistoryString() = "1, 45, 15, 38, 0, 49"
+History() = [41, 11, 14, 51, 0, 40]
+HistoryString() = "41, 11, 14, 51, 0, 40"
 IsChanceNode() = False
 IsSimultaneousNode() = False
 CurrentPlayer() = 0
-ObservationString(0) = "Non-Ace Total: 19 13 Num Aces: 0 0, Player's Turn\n"
+ObservationString(0) = "Non-Ace Total: 15 12 Num Aces: 0 0, Player's Turn\n"
+ObservationTensor(0): ◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉
 Rewards() = [0]
 Returns() = [0]
 LegalActions() = [0, 1]
 StringLegalActions() = ["Hit", "Stand"]
 
-# Apply action "Stand"
-action: 1
+# Apply action "Hit"
+action: 0
 
 # State 7
-# Apply action "D5"
-action: 17
+# Apply action "HK"
+action: 38
 
 # State 8
-# Non-Ace Total: 19 18 Num Aces: 0 0, Player's Turn
+# Non-Ace Total: 25 12 Num Aces: 0 0, Player's Turn
 IsTerminal() = True
-History() = [1, 45, 15, 38, 0, 49, 1, 17]
-HistoryString() = "1, 45, 15, 38, 0, 49, 1, 17"
+History() = [41, 11, 14, 51, 0, 40, 0, 38]
+HistoryString() = "41, 11, 14, 51, 0, 40, 0, 38"
 IsChanceNode() = False
 IsSimultaneousNode() = False
 CurrentPlayer() = -4
-ObservationString(0) = "Non-Ace Total: 19 18 Num Aces: 0 0, Player's Turn\n"
-Rewards() = [1]
-Returns() = [1]
+ObservationString(0) = "Non-Ace Total: 25 12 Num Aces: 0 0, Player's Turn\n"
+ObservationTensor(0): ◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◉◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◉
+Rewards() = [-1]
+Returns() = [-1]